This commit is contained in:
2026-02-20 04:32:50 +00:00
parent d7503ca56b
commit d9f6eb14ed
24 changed files with 1426 additions and 1183 deletions
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
import { useState, useMemo, useRef, useEffect, type KeyboardEvent } from 'react';
import { useNavigate } from 'react-router';
import {
Send,
@@ -23,18 +23,32 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useSettings } from '@/state/useSettings';
import { useVisibleClaudeModels, useVisibleOpenCodeModels, useVisiblePiMonoModels } from '@/state/useModels';
import { useVisiblePiMonoModels } from '@/state/useModels';
import type { Attachment } from '@/Screens/Dashboard/Chat/EmbeddableChat';
const PROVIDER_DISPLAY: Record<string, string> = {
anthropic: 'Anthropic',
openai: 'OpenAI',
opencode: 'OpenCode Zen',
google: 'Google',
groq: 'Groq',
mistral: 'Mistral',
xai: 'xAI',
openrouter: 'OpenRouter',
huggingface: 'Hugging Face',
'github-copilot': 'GitHub Copilot',
minimax: 'MiniMax',
bedrock: 'Amazon Bedrock',
'google-vertex': 'Google Vertex AI',
'azure-openai': 'Azure OpenAI',
};
export const ChatLauncher = () => {
const navigate = useNavigate();
const { settings } = useSettings();
const claudeModels = useVisibleClaudeModels();
const openCodeModels = useVisibleOpenCodeModels();
const piMonoModels = useVisiblePiMonoModels();
const client = useClient();
const [provider, setProvider] = useState<'claude' | 'opencode' | 'pi-mono'>(settings.chat.defaultProvider);
const [model, setModel] = useState<string | null>(settings.chat.defaultModel);
const [input, setInput] = useState('');
const [attachments, setAttachments] = useState<Attachment[]>([]);
@@ -44,11 +58,23 @@ export const ChatLauncher = () => {
const imageInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setProvider(settings.chat.defaultProvider);
setModel(settings.chat.defaultModel);
}, [settings.chat.defaultProvider, settings.chat.defaultModel]);
}, [settings.chat.defaultModel]);
const models = provider === 'claude' ? claudeModels : provider === 'opencode' ? openCodeModels : piMonoModels;
const providers = useMemo(
() => [...new Set(piMonoModels.map((m) => m.provider).filter(Boolean))] as string[],
[piMonoModels],
);
const activeProvider = piMonoModels.find((m) => m.id === model)?.provider ?? providers[0];
const providerModels = piMonoModels.filter((m) => m.provider === activeProvider);
const displayName = (provider: string) => PROVIDER_DISPLAY[provider] ?? provider;
const handleProviderClick = (provider: string) => {
const firstModel = piMonoModels.find((m) => m.provider === provider);
if (firstModel) setModel(firstModel.id);
};
const handleAttachWebpage = async (url: string) => {
const idx = attachments.length;
@@ -60,7 +86,7 @@ export const ChatLauncher = () => {
try {
const res = await client.post<{ url: string; title: string; content: string; attachmentId: string }>('/scrape', {
url,
provider,
provider: 'pi-mono',
});
setAttachments((prev) =>
prev.map((a, i) =>
@@ -85,7 +111,7 @@ export const ChatLauncher = () => {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('provider', provider);
formData.append('provider', 'pi-mono');
const res = await client.post<{ filename: string; dataUrl: string; attachmentId: string }>('/upload', formData);
setAttachments((prev) =>
@@ -125,9 +151,7 @@ export const ChatLauncher = () => {
attachmentIds.push(a.attachmentId);
}
const route =
provider === 'claude' ? '/chat/new' : provider === 'opencode' ? '/chat/opencode/new' : '/chat/pi-mono/new';
navigate(route, {
navigate('/chat/new', {
state: {
initialMessage: prompt,
model,
@@ -259,40 +283,36 @@ export const ChatLauncher = () => {
<div className="flex items-center justify-between px-4 pb-3">
<div className="flex items-center gap-1 rounded-lg bg-duck-dark/5 p-1">
{(['claude', 'opencode', 'pi-mono'] as const).map((value) => (
{providers.map((provider) => (
<button
key={value}
onClick={() => {
setProvider(value);
setModel(null);
}}
key={provider}
onClick={() => handleProviderClick(provider)}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors cursor-pointer ${
provider === value
activeProvider === provider
? 'bg-background text-duck-dark shadow-sm'
: 'text-duck-dark/50 hover:text-duck-dark/70'
}`}
>
{value === 'claude' ? 'Claude' : value === 'opencode' ? 'OpenCode' : 'Pi'}
{displayName(provider)}
</button>
))}
</div>
{models.length > 0 && (
{providerModels.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-1 text-xs text-duck-dark/50 hover:text-duck-dark/70 cursor-pointer transition-colors">
{models.find((m) => m.id === (model ?? models[0]?.id))?.name ?? models[0]?.name}
{providerModels.find((m) => m.id === (model ?? providerModels[0]?.id))?.name ?? providerModels[0]?.name}
<ChevronDown className="h-3 w-3" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="z-[600] max-h-64 overflow-y-auto">
{models.map((m) => (
{providerModels.map((m) => (
<DropdownMenuItem key={m.id} onClick={() => setModel(m.id)} className="cursor-pointer">
<Check
className={`mr-2 h-3 w-3 ${(model ?? models[0]?.id) === m.id ? 'opacity-100' : 'opacity-0'}`}
className={`mr-2 h-3 w-3 ${(model ?? providerModels[0]?.id) === m.id ? 'opacity-100' : 'opacity-0'}`}
/>
<span className="font-bold">{m.name}</span>
{m.provider && <span className="text-duck-dark/50 ml-1">({m.provider})</span>}
</DropdownMenuItem>
))}
</DropdownMenuContent>