provider, model, and mode selections

User can now update the AI Provider, selected model on that provider,
and chat session mode.
This commit is contained in:
Rob Colbert 2026-05-05 20:49:13 -04:00
parent cb73d276a3
commit 819654e20a
2 changed files with 191 additions and 18 deletions

View File

@ -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( requestSessionLock(
registration: any, registration: any,
project: any, project: any,

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useContext, useCallback } from 'react'; import { useState, useEffect, useRef, useContext, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { socketClient } from '../lib/socket'; 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 { WorkspaceMode } from '../lib/types';
import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator'; import WorkspaceModeIndicator from '../components/WorkspaceModeIndicator';
import FilesPanel from '../components/FilesPanel'; import FilesPanel from '../components/FilesPanel';
@ -30,9 +30,15 @@ export default function ChatSessionView() {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [sessionLocked, setSessionLocked] = useState(true); const [sessionLocked, setSessionLocked] = useState(true);
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>(WorkspaceMode.Idle); const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>(WorkspaceMode.Idle);
const [isUpdatingMode, setIsUpdatingMode] = useState(false);
const [isUpdatingProvider, setIsUpdatingProvider] = useState(false);
const [isUpdatingModel, setIsUpdatingModel] = useState(false);
const [logExpanded, setLogExpanded] = useState(false); const [logExpanded, setLogExpanded] = useState(false);
const [logs, setLogs] = useState<LogEntry[]>([]); const [logs, setLogs] = useState<LogEntry[]>([]);
const [toast, setToast] = useState<string | null>(null); const [toast, setToast] = useState<string | null>(null);
const [providers, setProviders] = useState<AiProvider[]>([]);
const [selectedProviderId, setSelectedProviderId] = useState<string>('');
const [selectedModelId, setSelectedModelId] = useState<string>('');
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
@ -81,6 +87,15 @@ export default function ChatSessionView() {
const turnsData = await chatSessionApi.getTurns(sessionId); const turnsData = await chatSessionApi.getTurns(sessionId);
setTurns(turnsData); setTurns(turnsData);
setSessionLocked(true); 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) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load session'); setError(err instanceof Error ? err.message : 'Failed to load session');
@ -257,13 +272,91 @@ export default function ChatSessionView() {
} }
}; };
const handleModeChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
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<HTMLSelectElement>) => {
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<HTMLSelectElement>) => {
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) => { const handleSubmitPrompt = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!promptInput.trim() || isProcessing || !socketClient.connected || !sessionLocked) return; if (!promptInput.trim() || isProcessing || !socketClient.connected || !sessionLocked) return;
if (workspaceMode !== WorkspaceMode.Agent) 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 = { const userTurn: ChatTurn = {
_id: `temp-${Date.now()}`, _id: tempTurnId,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
user: session?.user?._id || '', user: session?.user?._id || '',
project: session?.project?._id || projectId || '', project: session?.project?._id || projectId || '',
@ -288,12 +381,33 @@ export default function ChatSessionView() {
}; };
setTurns(prev => [...prev, userTurn]); setTurns(prev => [...prev, userTurn]);
currentTurnIdRef.current = userTurn._id; currentTurnIdRef.current = tempTurnId;
setPromptInput(''); setPromptInput('');
setIsProcessing(true); setIsProcessing(true);
setError(''); 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 = () => { 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 const promptPlaceholder = workspaceMode !== WorkspaceMode.Agent
? 'Select Agent mode to enter a prompt.' ? 'Select Agent mode to enter a prompt.'
: !session?.selectedModel
? 'Please select a model first.'
: 'Enter your prompt... (Shift+Enter for new line)'; : 'Enter your prompt... (Shift+Enter for new line)';
if (loading) { if (loading) {
@ -414,20 +530,66 @@ export default function ChatSessionView() {
disabled={!sessionLocked} disabled={!sessionLocked}
/> />
</div> </div>
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
<div className="flex items-center gap-1 text-green-500"> <div>
<span></span> <div className="text-xs text-text-muted">Name</div>
<span className="text-xs">{sessionLocked ? 'Locked' : 'Unlocked'}</span> <div className="text-text-primary">{session?.name}</div>
</div>
<div className="flex gap-2">
<div className="flex-1">
<div className="text-xs text-text-muted">Provider</div>
<select
value={selectedProviderId || ''}
onChange={handleProviderChange}
disabled={isUpdatingProvider}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{providers.map((provider) => (
<option key={provider._id} value={provider._id}>
{provider.name}
</option>
))}
</select>
</div>
<div className="flex-1">
<div className="text-xs text-text-muted">Model</div>
<select
value={selectedModelId || ''}
onChange={handleModelChange}
disabled={isUpdatingModel || !selectedProviderId}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed truncate"
>
{!selectedProviderId ? (
<option value="">Select provider first</option>
) : (
<>
<option value="">-- Select Model --</option>
{getSortedModels(selectedProviderId).map((model) => (
<option key={model.id} value={model.id}>
{truncateModelName(model.name)}
</option>
))}
</>
)}
</select>
</div>
</div>
<div>
<div className="text-xs text-text-muted">Mode</div>
<select
value={session?.mode || ChatSessionMode.Build}
onChange={handleModeChange}
disabled={isUpdatingMode}
className="w-full mt-1 px-2 py-1.5 bg-bg-tertiary border border-border-default rounded text-text-primary text-sm focus:border-brand focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{Object.values(ChatSessionMode).map((mode) => (
<option key={mode} value={mode}>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</option>
))}
</select>
</div>
</div> </div>
<div>
<div className="text-xs text-text-muted">Name</div>
<div className="text-text-primary">{session?.name}</div>
</div>
<div>
<div className="text-xs text-text-muted">Model</div>
<div className="text-text-primary">{session?.selectedModel}</div>
</div>
</div>
</div> </div>
{/* PROJECT Panel */} {/* PROJECT Panel */}