mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-20 20:41:31 +00:00
307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
import React, { useCallback, useMemo, useState } from "react";
|
|
import { Check, ChevronDown } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import SessionProviderLogo from "../../../llm-logo-provider/SessionProviderLogo";
|
|
import {
|
|
CLAUDE_MODELS,
|
|
CURSOR_MODELS,
|
|
CODEX_MODELS,
|
|
GEMINI_MODELS,
|
|
} from "../../../../../shared/modelConstants";
|
|
import type { ProjectSession, LLMProvider } from "../../../../types/app";
|
|
import { NextTaskBanner } from "../../../task-master";
|
|
import {
|
|
Dialog,
|
|
DialogTrigger,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Command,
|
|
CommandInput,
|
|
CommandList,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandItem,
|
|
Card,
|
|
} from "../../../../shared/view/ui";
|
|
|
|
type ProviderSelectionEmptyStateProps = {
|
|
selectedSession: ProjectSession | null;
|
|
currentSessionId: string | null;
|
|
provider: LLMProvider;
|
|
setProvider: (next: LLMProvider) => void;
|
|
textareaRef: React.RefObject<HTMLTextAreaElement>;
|
|
claudeModel: string;
|
|
setClaudeModel: (model: string) => void;
|
|
cursorModel: string;
|
|
setCursorModel: (model: string) => void;
|
|
codexModel: string;
|
|
setCodexModel: (model: string) => void;
|
|
geminiModel: string;
|
|
setGeminiModel: (model: string) => void;
|
|
tasksEnabled: boolean;
|
|
isTaskMasterInstalled: boolean | null;
|
|
onShowAllTasks?: (() => void) | null;
|
|
setInput: React.Dispatch<React.SetStateAction<string>>;
|
|
};
|
|
|
|
interface ProviderGroup {
|
|
id: LLMProvider;
|
|
name: string;
|
|
models: { value: string; label: string }[];
|
|
}
|
|
|
|
const PROVIDER_GROUPS: ProviderGroup[] = [
|
|
{ id: "claude", name: "Anthropic", models: CLAUDE_MODELS.OPTIONS },
|
|
{ id: "cursor", name: "Cursor", models: CURSOR_MODELS.OPTIONS },
|
|
{ id: "codex", name: "OpenAI", models: CODEX_MODELS.OPTIONS },
|
|
{ id: "gemini", name: "Google", models: GEMINI_MODELS.OPTIONS },
|
|
];
|
|
|
|
function getModelConfig(p: LLMProvider) {
|
|
if (p === "claude") return CLAUDE_MODELS;
|
|
if (p === "codex") return CODEX_MODELS;
|
|
if (p === "gemini") return GEMINI_MODELS;
|
|
return CURSOR_MODELS;
|
|
}
|
|
|
|
function getCurrentModel(
|
|
p: LLMProvider,
|
|
c: string,
|
|
cu: string,
|
|
co: string,
|
|
g: string,
|
|
) {
|
|
if (p === "claude") return c;
|
|
if (p === "codex") return co;
|
|
if (p === "gemini") return g;
|
|
return cu;
|
|
}
|
|
|
|
function getProviderDisplayName(p: LLMProvider) {
|
|
if (p === "claude") return "Claude";
|
|
if (p === "cursor") return "Cursor";
|
|
if (p === "codex") return "Codex";
|
|
return "Gemini";
|
|
}
|
|
|
|
export default function ProviderSelectionEmptyState({
|
|
selectedSession,
|
|
currentSessionId,
|
|
provider,
|
|
setProvider,
|
|
textareaRef,
|
|
claudeModel,
|
|
setClaudeModel,
|
|
cursorModel,
|
|
setCursorModel,
|
|
codexModel,
|
|
setCodexModel,
|
|
geminiModel,
|
|
setGeminiModel,
|
|
tasksEnabled,
|
|
isTaskMasterInstalled,
|
|
onShowAllTasks,
|
|
setInput,
|
|
}: ProviderSelectionEmptyStateProps) {
|
|
const { t } = useTranslation("chat");
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const nextTaskPrompt = t("tasks.nextTaskPrompt", {
|
|
defaultValue: "Start the next task",
|
|
});
|
|
|
|
const currentModel = getCurrentModel(
|
|
provider,
|
|
claudeModel,
|
|
cursorModel,
|
|
codexModel,
|
|
geminiModel,
|
|
);
|
|
|
|
const currentModelLabel = useMemo(() => {
|
|
const config = getModelConfig(provider);
|
|
const found = config.OPTIONS.find(
|
|
(o: { value: string; label: string }) => o.value === currentModel,
|
|
);
|
|
return found?.label || currentModel;
|
|
}, [provider, currentModel]);
|
|
|
|
const handleModelSelect = useCallback(
|
|
(providerId: LLMProvider, modelValue: string) => {
|
|
// Set provider
|
|
setProvider(providerId);
|
|
localStorage.setItem("selected-provider", providerId);
|
|
|
|
// Set model for the correct provider
|
|
if (providerId === "claude") {
|
|
setClaudeModel(modelValue);
|
|
localStorage.setItem("claude-model", modelValue);
|
|
} else if (providerId === "codex") {
|
|
setCodexModel(modelValue);
|
|
localStorage.setItem("codex-model", modelValue);
|
|
} else if (providerId === "gemini") {
|
|
setGeminiModel(modelValue);
|
|
localStorage.setItem("gemini-model", modelValue);
|
|
} else {
|
|
setCursorModel(modelValue);
|
|
localStorage.setItem("cursor-model", modelValue);
|
|
}
|
|
|
|
setDialogOpen(false);
|
|
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
},
|
|
[setProvider, setClaudeModel, setCursorModel, setCodexModel, setGeminiModel, textareaRef],
|
|
);
|
|
|
|
/* ── New session — provider + model picker ── */
|
|
if (!selectedSession && !currentSessionId) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center px-4">
|
|
<div className="w-full max-w-md">
|
|
{/* Heading */}
|
|
<div className="mb-8 text-center">
|
|
<h2 className="text-lg font-semibold tracking-tight text-foreground sm:text-xl">
|
|
{t("providerSelection.title")}
|
|
</h2>
|
|
<p className="mt-1 text-[13px] text-muted-foreground">
|
|
{t("providerSelection.description")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Model selector trigger — hero card style */}
|
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Card
|
|
className="group mx-auto max-w-sm cursor-pointer border-border/60 transition-all duration-150 hover:border-border hover:shadow-md active:scale-[0.99]"
|
|
role="button"
|
|
tabIndex={0}
|
|
>
|
|
<div className="flex items-center gap-3 p-4">
|
|
<SessionProviderLogo
|
|
provider={provider}
|
|
className="h-8 w-8 shrink-0"
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-sm font-semibold text-foreground">
|
|
{getProviderDisplayName(provider)}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">·</span>
|
|
<span className="truncate text-sm text-foreground">
|
|
{currentModelLabel}
|
|
</span>
|
|
</div>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
{t("providerSelection.clickToChange", {
|
|
defaultValue: "Click to change model",
|
|
})}
|
|
</p>
|
|
</div>
|
|
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-y-0.5" />
|
|
</div>
|
|
</Card>
|
|
</DialogTrigger>
|
|
|
|
<DialogContent className="max-w-md overflow-hidden p-0">
|
|
<DialogTitle>Model Selector</DialogTitle>
|
|
<Command>
|
|
<CommandInput placeholder={t("providerSelection.searchModels", { defaultValue: "Search models..." })} />
|
|
<CommandList className="max-h-[350px]">
|
|
<CommandEmpty>
|
|
{t("providerSelection.noModelsFound", { defaultValue: "No models found." })}
|
|
</CommandEmpty>
|
|
{PROVIDER_GROUPS.map((group) => (
|
|
<CommandGroup
|
|
key={group.id}
|
|
heading={
|
|
<span className="flex items-center gap-1.5">
|
|
<SessionProviderLogo provider={group.id} className="h-3.5 w-3.5 shrink-0" />
|
|
{group.name}
|
|
</span>
|
|
}
|
|
>
|
|
{group.models.map((model) => {
|
|
const isSelected =
|
|
provider === group.id && currentModel === model.value;
|
|
return (
|
|
<CommandItem
|
|
key={`${group.id}-${model.value}`}
|
|
value={`${group.name} ${model.label}`}
|
|
onSelect={() => handleModelSelect(group.id, model.value)}
|
|
>
|
|
<span className="flex-1 truncate">{model.label}</span>
|
|
{isSelected && (
|
|
<Check className="ml-auto h-4 w-4 shrink-0 text-primary" />
|
|
)}
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
))}
|
|
</CommandList>
|
|
</Command>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Ready prompt */}
|
|
<p className="mt-4 text-center text-sm text-muted-foreground/70">
|
|
{
|
|
{
|
|
claude: t("providerSelection.readyPrompt.claude", {
|
|
model: claudeModel,
|
|
}),
|
|
cursor: t("providerSelection.readyPrompt.cursor", {
|
|
model: cursorModel,
|
|
}),
|
|
codex: t("providerSelection.readyPrompt.codex", {
|
|
model: codexModel,
|
|
}),
|
|
gemini: t("providerSelection.readyPrompt.gemini", {
|
|
model: geminiModel,
|
|
}),
|
|
}[provider]
|
|
}
|
|
</p>
|
|
|
|
{/* Task banner */}
|
|
{provider && tasksEnabled && isTaskMasterInstalled && (
|
|
<div className="mt-5">
|
|
<NextTaskBanner
|
|
onStartTask={() => setInput(nextTaskPrompt)}
|
|
onShowAllTasks={onShowAllTasks}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Existing session — continue prompt ── */
|
|
if (selectedSession) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center">
|
|
<div className="max-w-md px-6 text-center">
|
|
<p className="mb-1.5 text-lg font-semibold text-foreground">
|
|
{t("session.continue.title")}
|
|
</p>
|
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
{t("session.continue.description")}
|
|
</p>
|
|
|
|
{tasksEnabled && isTaskMasterInstalled && (
|
|
<div className="mt-5">
|
|
<NextTaskBanner
|
|
onStartTask={() => setInput(nextTaskPrompt)}
|
|
onShowAllTasks={onShowAllTasks}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|