import React, { useState, useRef, useEffect, useCallback } from "react";
import useVoiceChat from "../hooks/useVoiceChat";
import ChatWindow from "../components/ChatWindow";
import Controls from "../components/VoiceControls";
import SessionSidebar from "../components/SessionSidebar";
import { updateUserConfig } from "../services/apiService";
const VoiceChatPage = ({ Icon }) => {
const chatContainerRef = useRef(null);
const [showConfigModal, setShowConfigModal] = useState(false);
const [savingConfig, setSavingConfig] = useState(false);
const {
chatHistory,
status,
isRecording,
isBusy,
isAutoMode,
isAutoListening,
showErrorModal,
errorMessage,
tokenUsage,
setIsAutoMode,
handleMicClick,
handleNewSession,
setShowErrorModal,
handleSwitchSession,
sessionId,
isConfigured,
missingConfigs,
userConfigData,
localActivePrefs,
setLocalActivePrefs,
setErrorMessage
} = useVoiceChat({ chatContainerRef });
useEffect(() => {
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
}, [chatHistory]);
const toggleAutoMode = () => {
setIsAutoMode(!isAutoMode);
};
const handleSaveQuickConfig = async () => {
try {
setSavingConfig(true);
// The localActivePrefs are already updated via setLocalActivePrefs
// Since our API requests natively use these local states directly, it applies instantly to the CURRENT session!
setShowConfigModal(false);
} catch (e) {
console.error(e);
setErrorMessage("Failed to apply the session-local providers.");
setShowErrorModal(true);
} finally {
setSavingConfig(false);
}
};
return (
<div className="flex flex-col flex-grow h-full h-screen max-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 relative overflow-hidden">
<SessionSidebar
featureName="voice_chat"
currentSessionId={sessionId}
onSwitchSession={handleSwitchSession}
onNewSession={handleNewSession}
/>
{/* Header bar - Fixed height */}
<div className="bg-white dark:bg-gray-800 p-4 shadow flex justify-between items-center z-10 w-full flex-shrink-0">
<h2 className="text-xl font-bold flex items-center gap-3">
Voice Chat Assistant
{!isConfigured && (
<div className="group relative flex items-center">
<svg className="w-6 h-6 text-yellow-500 cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
{/* Tooltip on hover */}
<div className="absolute left-0 top-full mt-2 w-64 bg-gray-900 text-white text-xs rounded shadow-lg p-3 opacity-0 group-hover:opacity-100 transition-opacity z-50 pointer-events-none">
<p className="font-bold mb-1 text-red-400">Configuration Missing</p>
<ul className="list-disc pl-4 space-y-1">
{missingConfigs?.map((m, i) => <li key={i}>{m}</li>)}
</ul>
</div>
</div>
)}
<button
onClick={() => setShowConfigModal(true)}
title="Quick Settings - Swap Active Providers"
className="text-gray-400 hover:text-indigo-500 dark:hover:text-indigo-400 transition-colors bg-gray-50 dark:bg-gray-700/50 p-1.5 rounded-full"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
</button>
</h2>
<div className="flex items-center space-x-4">
<div className="text-sm font-mono text-gray-500 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
{tokenUsage?.token_count || 0} / {tokenUsage?.token_limit || 100000} tokens ({tokenUsage?.percentage || 0}%)
</div>
<button
onClick={handleNewSession}
className="text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 dark:bg-blue-900 dark:hover:bg-blue-800 dark:text-blue-200 px-3 py-1 rounded font-semibold transition-colors"
>
+ New Session
</button>
</div>
</div>
{/* Main Chat Area - Scrollable */}
<div className="flex-grow overflow-y-auto w-full px-4 py-6" ref={chatContainerRef}>
<ChatWindow chatHistory={chatHistory} />
</div>
{/* Footer Controls - Fixed height */}
<div className="flex-shrink-0 z-10 w-full border-t border-gray-200 dark:border-gray-700">
<Controls
status={status}
isBusy={isBusy}
isRecording={isRecording}
isAutoMode={isAutoMode}
isAutoListening={isAutoListening}
onMicClick={handleMicClick}
onToggleAutoMode={toggleAutoMode}
/>
</div>
{showErrorModal && (
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 flex justify-center items-center z-50">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl text-center">
<h2 className="text-xl font-bold mb-4 text-red-500">Error</h2>
<p className="mb-4">{errorMessage}</p>
<button
onClick={() => setShowErrorModal(false)}
className="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Close
</button>
</div>
</div>
)}
{/* Quick Settings Modal */}
{showConfigModal && (
<div className="fixed inset-0 bg-gray-900/60 backdrop-blur-sm flex justify-center items-center z-50">
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-2xl w-full max-w-sm border border-gray-200 dark:border-gray-700 animate-fade-in relative">
<h2 className="text-lg font-bold mb-4 text-gray-900 dark:text-white flex items-center gap-2">
<svg className="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
Quick Session Settings
</h2>
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold text-gray-500 dark:text-gray-400 mb-1 uppercase tracking-wider">Active LLM</label>
<select
className="w-full bg-gray-50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded p-2 text-sm text-gray-800 dark:text-gray-200"
value={localActivePrefs.llm}
onChange={(e) => setLocalActivePrefs({ ...localActivePrefs, llm: e.target.value })}
>
{Object.keys(userConfigData?.effective?.llm?.providers || {}).map(k => {
const modelName = userConfigData.effective.llm.providers[k].model;
return (
<option key={k} value={k}>
{k} {modelName ? `(${modelName})` : ''}
</option>
);
})}
{Object.keys(userConfigData?.effective?.llm?.providers || {}).length === 0 && <option value="">No configured providers</option>}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-500 dark:text-gray-400 mb-1 uppercase tracking-wider">Active TTS (Speech)</label>
<select
className="w-full bg-gray-50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded p-2 text-sm text-gray-800 dark:text-gray-200"
value={localActivePrefs.tts}
onChange={(e) => setLocalActivePrefs({ ...localActivePrefs, tts: e.target.value })}
>
{Object.keys(userConfigData?.effective?.tts?.providers || {}).map(k => (
<option key={k} value={k}>{k}</option>
))}
{Object.keys(userConfigData?.effective?.tts?.providers || {}).length === 0 && <option value="">No configured providers</option>}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-500 dark:text-gray-400 mb-1 uppercase tracking-wider">Active STT (Listening)</label>
<select
className="w-full bg-gray-50 dark:bg-gray-700/50 border border-gray-300 dark:border-gray-600 rounded p-2 text-sm text-gray-800 dark:text-gray-200"
value={localActivePrefs.stt}
onChange={(e) => setLocalActivePrefs({ ...localActivePrefs, stt: e.target.value })}
>
{Object.keys(userConfigData?.effective?.stt?.providers || {}).map(k => (
<option key={k} value={k}>{k}</option>
))}
{Object.keys(userConfigData?.effective?.stt?.providers || {}).length === 0 && <option value="">No configured providers</option>}
</select>
</div>
</div>
<div className="mt-6 flex justify-end space-x-3">
<button
onClick={() => setShowConfigModal(false)}
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-200 text-gray-800 rounded font-semibold text-sm transition-colors"
>
Cancel
</button>
<button
disabled={savingConfig}
onClick={handleSaveQuickConfig}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded font-bold text-sm shadow-sm transition-all flex items-center gap-2"
>
{savingConfig ? (
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
) : 'Apply to Current Session'}
</button>
</div>
<p className="mt-4 text-[10px] text-gray-400 text-center">To add new providers or API keys, visit Settings.</p>
</div>
</div>
)}
</div>
);
};
export default VoiceChatPage;