fix(webui): route first-run model setup choices
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useRef } from "react";
|
||||
import { Check, Cloud, KeyRound, Laptop } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -9,14 +10,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ModelSetupAvailability {
|
||||
account: boolean;
|
||||
apiKey: boolean;
|
||||
local: boolean;
|
||||
}
|
||||
|
||||
export type ModelSetupIntent = keyof ModelSetupAvailability;
|
||||
import type { ModelSetupAvailability, ModelSetupIntent } from "@/lib/model-setup";
|
||||
|
||||
const SETUP_OPTIONS = [
|
||||
{
|
||||
@@ -59,14 +53,22 @@ export function ModelSetupDialog({
|
||||
onSelect: (intent: ModelSetupIntent) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const selectedRef = useRef(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nextOpen) selectedRef.current = false;
|
||||
onOpenChange(nextOpen);
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-w-md gap-5 p-5 sm:p-6"
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
onReturnFocus();
|
||||
if (!selectedRef.current) onReturnFocus();
|
||||
selectedRef.current = false;
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="pr-7">
|
||||
@@ -89,7 +91,10 @@ export function ModelSetupDialog({
|
||||
key={option.intent}
|
||||
type="button"
|
||||
aria-label={t(option.titleKey, { defaultValue: option.title })}
|
||||
onClick={() => onSelect(option.intent)}
|
||||
onClick={() => {
|
||||
selectedRef.current = true;
|
||||
onSelect(option.intent);
|
||||
}}
|
||||
className={cn(
|
||||
"group flex min-h-[68px] w-full items-center gap-3 rounded-control border border-border/55 bg-background px-3.5 py-3 text-left",
|
||||
"transition-[background-color,border-color,transform] duration-150 ease-out hover:border-border hover:bg-muted/45 active:scale-[0.99]",
|
||||
|
||||
@@ -70,10 +70,7 @@ import {
|
||||
ModelPresetBadge,
|
||||
type ModelPresetOption,
|
||||
} from "@/components/thread/ModelPresetBadge";
|
||||
import {
|
||||
ModelSetupDialog,
|
||||
type ModelSetupAvailability,
|
||||
} from "@/components/thread/ModelSetupDialog";
|
||||
import { ModelSetupDialog } from "@/components/thread/ModelSetupDialog";
|
||||
import {
|
||||
ACCEPT_ATTR,
|
||||
MAX_ATTACHMENTS_PER_MESSAGE,
|
||||
@@ -117,6 +114,7 @@ import {
|
||||
} from "@/lib/session-drag";
|
||||
import { formatQuotedUserMessage } from "@/lib/user-message-quote";
|
||||
import { formatCompactTokenCount } from "@/lib/format";
|
||||
import type { ModelSetupAvailability, ModelSetupIntent } from "@/lib/model-setup";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const VOICE_SHORTCUT_CODE = "KeyD";
|
||||
@@ -304,7 +302,7 @@ interface ThreadComposerProps {
|
||||
modelNeedsSetup?: boolean;
|
||||
modelSetupAvailability?: ModelSetupAvailability;
|
||||
fallbackModelName?: string | null;
|
||||
onModelBadgeClick?: () => void;
|
||||
onModelBadgeClick?: (intent?: ModelSetupIntent) => void;
|
||||
onManageModels?: () => void;
|
||||
contextUsage?: ComposerContextUsage | null;
|
||||
variant?: "thread" | "hero";
|
||||
@@ -2137,9 +2135,9 @@ export function ThreadComposer({
|
||||
setModelSetupOpen(true);
|
||||
}, []);
|
||||
|
||||
const continueModelSetup = useCallback(() => {
|
||||
const continueModelSetup = useCallback((intent: ModelSetupIntent) => {
|
||||
setModelSetupOpen(false);
|
||||
onModelBadgeClick?.();
|
||||
onModelBadgeClick?.(intent);
|
||||
}, [onModelBadgeClick]);
|
||||
|
||||
const onKeyDown = (e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type ComposerContextUsage,
|
||||
} from "@/components/thread/ThreadComposer";
|
||||
import type { ModelPresetOption } from "@/components/thread/ModelPresetBadge";
|
||||
import type { ModelSetupAvailability } from "@/components/thread/ModelSetupDialog";
|
||||
import { ThreadHeader } from "@/components/thread/ThreadHeader";
|
||||
import { StreamErrorNotice } from "@/components/thread/StreamErrorNotice";
|
||||
import { ThreadViewport, type ThreadViewportHandle } from "@/components/thread/ThreadViewport";
|
||||
@@ -40,6 +39,10 @@ import {
|
||||
} from "@/lib/mcp-preset-events";
|
||||
import type { CanonicalRunSnapshot, StreamError } from "@/lib/nanobot-client";
|
||||
import { inferProviderFromModelName, providerDisplayLabel } from "@/lib/provider-brand";
|
||||
import {
|
||||
modelSetupAvailability,
|
||||
type ModelSetupIntent,
|
||||
} from "@/lib/model-setup";
|
||||
import type {
|
||||
ChatSummary,
|
||||
SettingsPayload,
|
||||
@@ -355,6 +358,7 @@ interface ThreadShellProps {
|
||||
composerPortalTarget?: HTMLElement | null;
|
||||
composerActive?: boolean;
|
||||
composerInputAriaLabel?: string;
|
||||
focusComposerRequest?: number;
|
||||
emptyComposerVariant?: "hero" | "thread";
|
||||
workspaceScope?: WorkspaceScopePayload | null;
|
||||
workspaceDefaultScope?: WorkspaceScopePayload | null;
|
||||
@@ -363,7 +367,7 @@ interface ThreadShellProps {
|
||||
workspaceError?: string | null;
|
||||
onWorkspaceScopeChange?: (scope: WorkspaceScopePayload) => void;
|
||||
settingsSnapshot?: SettingsPayload | null;
|
||||
onOpenModelSettings?: () => void;
|
||||
onOpenModelSettings?: (intent?: ModelSetupIntent) => void;
|
||||
skills?: SkillSummary[];
|
||||
}
|
||||
|
||||
@@ -383,22 +387,6 @@ interface ModelBadgeInfo {
|
||||
needsSetup: boolean;
|
||||
}
|
||||
|
||||
const LOCAL_MODEL_PROVIDERS = new Set(["atomic_chat", "lm_studio", "ollama", "vllm"]);
|
||||
|
||||
function modelSetupAvailability(settings: SettingsPayload | null): ModelSetupAvailability {
|
||||
const configured = settings?.providers.filter((provider) => provider.configured) ?? [];
|
||||
const isLocal = (provider: SettingsPayload["providers"][number]) => {
|
||||
if (LOCAL_MODEL_PROVIDERS.has(provider.name)) return true;
|
||||
const apiBase = provider.api_base?.trim().toLowerCase() ?? "";
|
||||
return apiBase.includes("localhost") || apiBase.includes("127.0.0.1") || apiBase.includes("[::1]");
|
||||
};
|
||||
return {
|
||||
account: configured.some((provider) => provider.auth_type === "oauth"),
|
||||
apiKey: configured.some((provider) => provider.auth_type !== "oauth" && !isLocal(provider)),
|
||||
local: configured.some(isLocal),
|
||||
};
|
||||
}
|
||||
|
||||
function modelPresetForBadge(
|
||||
settings: SettingsPayload | null,
|
||||
scopedPreset: string | null,
|
||||
@@ -671,6 +659,7 @@ export function ThreadShell({
|
||||
composerPortalTarget,
|
||||
composerActive = true,
|
||||
composerInputAriaLabel,
|
||||
focusComposerRequest = 0,
|
||||
emptyComposerVariant = "hero",
|
||||
workspaceScope = null,
|
||||
workspaceDefaultScope = null,
|
||||
@@ -980,7 +969,10 @@ export function ThreadShell({
|
||||
const modelBadgeLabel = modelBadge.needsSetup
|
||||
? t("thread.composer.chooseAI", { defaultValue: "Choose your AI" })
|
||||
: modelBadge.label;
|
||||
const setupAvailability = useMemo(() => modelSetupAvailability(settings), [settings]);
|
||||
const setupAvailability = useMemo(
|
||||
() => modelSetupAvailability(settings?.providers),
|
||||
[settings?.providers],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (showHeroComposer && !wasShowingHeroComposerRef.current) {
|
||||
setHeroGreetingKey(randomHeroGreetingKey());
|
||||
@@ -1563,7 +1555,7 @@ export function ThreadShell({
|
||||
transcriptionProvider={settingsSnapshot?.transcription?.provider}
|
||||
ingressLimits={ingressLimits}
|
||||
quotedContext={quotedContext}
|
||||
focusRequest={composerFocusSignal}
|
||||
focusRequest={composerFocusSignal + focusComposerRequest}
|
||||
onQuotedContextChange={setQuotedContext}
|
||||
/>
|
||||
) : (
|
||||
@@ -1611,6 +1603,7 @@ export function ThreadShell({
|
||||
onWorkspaceScopeChange={onWorkspaceScopeChange}
|
||||
transcriptionProvider={settingsSnapshot?.transcription?.provider}
|
||||
ingressLimits={ingressLimits}
|
||||
focusRequest={composerFocusSignal + focusComposerRequest}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user