diff --git a/gadget-code/frontend/src/lib/socket.ts b/gadget-code/frontend/src/lib/socket.ts index 4a234ae..aa35365 100644 --- a/gadget-code/frontend/src/lib/socket.ts +++ b/gadget-code/frontend/src/lib/socket.ts @@ -211,6 +211,17 @@ class SocketClient { } } + submitPrompt( + prompt: string, + cb: (success: boolean, data: { turnId?: string; message?: string }) => void, + ): void { + if (this._socket?.connected) { + this._socket.emit('submitPrompt', prompt, cb); + } else { + cb(false, { message: 'Socket not connected' }); + } + } + requestSessionLock( registration: any, project: any, diff --git a/gadget-code/frontend/src/pages/ChatSessionView.tsx b/gadget-code/frontend/src/pages/ChatSessionView.tsx index 81af8ba..ecf940a 100644 --- a/gadget-code/frontend/src/pages/ChatSessionView.tsx +++ b/gadget-code/frontend/src/pages/ChatSessionView.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useContext, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { socketClient } from '../lib/socket'; -import { chatSessionApi, projectApi, type ChatSession, type ChatTurn } from '../lib/api'; +import { chatSessionApi, projectApi, providerApi, type ChatSession, type ChatTurn, ChatSessionMode, type AiProvider } from '../lib/api'; import { WorkspaceMode } from '../lib/types'; import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator'; import FilesPanel from '../components/FilesPanel'; @@ -30,9 +30,15 @@ export default function ChatSessionView() { const [error, setError] = useState(''); const [sessionLocked, setSessionLocked] = useState(true); const [workspaceMode, setWorkspaceMode] = useState(WorkspaceMode.Idle); + const [isUpdatingMode, setIsUpdatingMode] = useState(false); + const [isUpdatingProvider, setIsUpdatingProvider] = useState(false); + const [isUpdatingModel, setIsUpdatingModel] = useState(false); const [logExpanded, setLogExpanded] = useState(false); const [logs, setLogs] = useState([]); const [toast, setToast] = useState(null); + const [providers, setProviders] = useState([]); + const [selectedProviderId, setSelectedProviderId] = useState(''); + const [selectedModelId, setSelectedModelId] = useState(''); const messagesEndRef = useRef(null); const inputRef = useRef(null); @@ -81,6 +87,15 @@ export default function ChatSessionView() { const turnsData = await chatSessionApi.getTurns(sessionId); setTurns(turnsData); setSessionLocked(true); + + const allProviders = await providerApi.getAll(); + setProviders(allProviders); + + const providerId = typeof sessionData.provider === 'string' + ? sessionData.provider + : sessionData.provider?._id; + setSelectedProviderId(providerId || ''); + setSelectedModelId(sessionData.selectedModel || ''); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load session'); @@ -257,13 +272,91 @@ export default function ChatSessionView() { } }; + const handleModeChange = async (e: React.ChangeEvent) => { + if (!session) return; + + const newMode = e.target.value as ChatSessionMode; + if (newMode === session.mode) return; + + setIsUpdatingMode(true); + try { + const updatedSession = await chatSessionApi.update(session._id, { mode: newMode }); + setSession(updatedSession); + showToast(`Session mode changed to ${newMode}`); + } catch (err) { + showToast(`Failed to change mode: ${err instanceof Error ? err.message : 'Unknown error'}`); + } finally { + setIsUpdatingMode(false); + } + }; + + const handleProviderChange = async (e: React.ChangeEvent) => { + if (!session) return; + + const newProviderId = e.target.value; + if (newProviderId === selectedProviderId) return; + + setIsUpdatingProvider(true); + try { + const updatedSession = await chatSessionApi.update(session._id, { + provider: newProviderId, + selectedModel: '' + }); + setSession(updatedSession); + setSelectedProviderId(newProviderId); + setSelectedModelId(''); + showToast('Provider changed. Please select a model.'); + } catch (err) { + showToast(`Failed to change provider: ${err instanceof Error ? err.message : 'Unknown error'}`); + } finally { + setIsUpdatingProvider(false); + } + }; + + const handleModelChange = async (e: React.ChangeEvent) => { + if (!session) return; + + const newModelId = e.target.value; + if (newModelId === selectedModelId) return; + + setIsUpdatingModel(true); + try { + const updatedSession = await chatSessionApi.update(session._id, { + selectedModel: newModelId + }); + setSession(updatedSession); + setSelectedModelId(newModelId); + showToast(`Model changed`); + } catch (err) { + showToast(`Failed to change model: ${err instanceof Error ? err.message : 'Unknown error'}`); + } finally { + setIsUpdatingModel(false); + } + }; + + const getSortedModels = (providerId: string) => { + const provider = providers.find(p => p._id === providerId); + if (!provider) return []; + return [...provider.models].sort((a, b) => a.name.localeCompare(b.name)); + }; + + const truncateModelName = (name: string, maxLength = 25) => { + if (name.length <= maxLength) return name; + return name.slice(0, maxLength - 3) + '...'; + }; + const handleSubmitPrompt = async (e: React.FormEvent) => { e.preventDefault(); if (!promptInput.trim() || isProcessing || !socketClient.connected || !sessionLocked) return; if (workspaceMode !== WorkspaceMode.Agent) return; + if (!session?.selectedModel) { + showToast('Please select a model before submitting a prompt'); + return; + } + const tempTurnId = `temp-${Date.now()}`; const userTurn: ChatTurn = { - _id: `temp-${Date.now()}`, + _id: tempTurnId, createdAt: new Date().toISOString(), user: session?.user?._id || '', project: session?.project?._id || projectId || '', @@ -288,12 +381,33 @@ export default function ChatSessionView() { }; setTurns(prev => [...prev, userTurn]); - currentTurnIdRef.current = userTurn._id; + currentTurnIdRef.current = tempTurnId; setPromptInput(''); setIsProcessing(true); setError(''); - socketClient.emitServer('submitPrompt', promptInput.trim()); + socketClient.submitPrompt(promptInput.trim(), (success, data) => { + if (success && data.turnId) { + setTurns(prevTurns => + prevTurns.map(turn => + turn._id === tempTurnId + ? { ...turn, _id: data.turnId! } + : turn + ) + ); + currentTurnIdRef.current = data.turnId; + } else if (!success) { + setTurns(prevTurns => + prevTurns.map(turn => + turn._id === tempTurnId + ? { ...turn, status: 'error', errorMessage: data.message || 'Failed to submit prompt' } + : turn + ) + ); + setIsProcessing(false); + currentTurnIdRef.current = null; + } + }); }; const scrollToBottom = () => { @@ -308,9 +422,11 @@ export default function ChatSessionView() { ); }, []); - const promptDisabled = isProcessing || workspaceMode !== WorkspaceMode.Agent || !sessionLocked; + const promptDisabled = isProcessing || workspaceMode !== WorkspaceMode.Agent || !sessionLocked || !session?.selectedModel; const promptPlaceholder = workspaceMode !== WorkspaceMode.Agent ? 'Select Agent mode to enter a prompt.' + : !session?.selectedModel + ? 'Please select a model first.' : 'Enter your prompt... (Shift+Enter for new line)'; if (loading) { @@ -414,20 +530,66 @@ export default function ChatSessionView() { disabled={!sessionLocked} /> -
-
- - {sessionLocked ? 'Locked' : 'Unlocked'} +
+
+
Name
+
{session?.name}
+
+
+
+
Provider
+ +
+
+
Model
+ +
+
+
+
Mode
+ +
-
-
Name
-
{session?.name}
-
-
-
Model
-
{session?.selectedModel}
-
-
{/* PROJECT Panel */}