Support Dynamic Thinking Models (#23281)
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Extra Build (push) Blocked by required conditions

* Add ability to toggle thinking

* Disable thinking for descriptions automatically

* mypy

* Cleanup
This commit is contained in:
Nicolas Mowen
2026-05-21 12:54:23 -05:00
committed by GitHub
parent 555ef89800
commit 66a2417229
16 changed files with 410 additions and 175 deletions
+147
View File
@@ -0,0 +1,147 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { FaArrowUpLong, FaStop } from "react-icons/fa6";
import { LuBrain } from "react-icons/lu";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip";
import { ChatQuickReplies } from "@/components/chat/ChatQuickReplies";
import { ChatPaperclipButton } from "@/components/chat/ChatPaperclipButton";
type ChatComposerProps = {
input: string;
setInput: (value: string) => void;
sendMessage: (textOverride?: string) => void;
placeholder: string;
supportsThinking: boolean;
thinkingEnabled: boolean;
setThinkingEnabled: (value: boolean | undefined) => void;
isLoading?: boolean;
onStop?: () => void;
attachedEventId?: string | null;
onClearAttachment?: () => void;
onAttach?: (eventId: string) => void;
recentEventIds?: string[];
large?: boolean;
};
export function ChatComposer({
input,
setInput,
sendMessage,
placeholder,
supportsThinking,
thinkingEnabled,
setThinkingEnabled,
isLoading = false,
onStop,
attachedEventId,
onClearAttachment,
onAttach,
recentEventIds,
large = false,
}: ChatComposerProps) {
const { t } = useTranslation(["views/chat"]);
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
const showPaperclip = !!onAttach;
const showStop = isLoading && !!onStop;
return (
<div className="flex w-full flex-col items-stretch justify-center gap-2 rounded-xl bg-secondary p-3">
{attachedEventId && onClearAttachment && (
<div className="flex items-center">
<ChatAttachmentChip
eventId={attachedEventId}
mode="composer"
onRemove={onClearAttachment}
/>
</div>
)}
{attachedEventId && (
<ChatQuickReplies
onSend={(text) => sendMessage(text)}
disabled={isLoading}
/>
)}
<div className="flex w-full flex-row items-center gap-2">
{showPaperclip && (
<ChatPaperclipButton
recentEventIds={recentEventIds ?? []}
onAttach={onAttach!}
disabled={isLoading || attachedEventId != null}
/>
)}
{supportsThinking && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="sm"
variant={thinkingEnabled ? "select" : "ghost"}
aria-pressed={thinkingEnabled}
aria-label={t("thinking.toggle")}
className={cn(
"flex size-9 shrink-0 items-center justify-center rounded-full p-0",
!thinkingEnabled && "text-secondary-foreground",
)}
onClick={() => setThinkingEnabled(!thinkingEnabled)}
disabled={isLoading}
>
<LuBrain className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{t("thinking.toggle")}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Input
className={cn(
"w-full flex-1 border-transparent bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
large && "h-12 text-base",
)}
placeholder={placeholder}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
aria-busy={isLoading}
/>
{showStop ? (
<Button
variant="destructive"
className="size-10 shrink-0 rounded-full"
onClick={onStop}
>
<FaStop className="size-3" />
</Button>
) : (
<Button
variant="select"
className="size-10 shrink-0 rounded-full"
disabled={!input.trim() || isLoading}
onClick={() => sendMessage()}
>
<FaArrowUpLong className="size-4" />
</Button>
)}
</div>
</div>
);
}
+21 -26
View File
@@ -1,15 +1,22 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { FaArrowUpLong } from "react-icons/fa6";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import type { StartingRequest } from "@/types/chat";
import { ChatComposer } from "@/components/chat/ChatComposer";
type ChatStartingStateProps = {
onSendMessage: (message: string) => void;
supportsThinking: boolean;
thinkingEnabled: boolean;
setThinkingEnabled: (value: boolean | undefined) => void;
};
export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) {
export function ChatStartingState({
onSendMessage,
supportsThinking,
thinkingEnabled,
setThinkingEnabled,
}: ChatStartingStateProps) {
const { t } = useTranslation(["views/chat"]);
const [input, setInput] = useState("");
@@ -36,20 +43,13 @@ export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) {
onSendMessage(prompt);
};
const handleSubmit = () => {
const text = input.trim();
const handleSend = (textOverride?: string) => {
const text = (textOverride ?? input).trim();
if (!text) return;
onSendMessage(text);
setInput("");
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
return (
<div className="flex size-full flex-col items-center justify-center gap-6 p-8">
<div className="flex flex-col items-center gap-2">
@@ -77,22 +77,17 @@ export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) {
</div>
</div>
<div className="flex w-full max-w-2xl flex-row items-center gap-2 rounded-xl bg-secondary p-3">
<Input
className="h-12 w-full flex-1 border-transparent bg-transparent text-base shadow-none focus-visible:ring-0 dark:bg-transparent"
<div className="w-full max-w-2xl">
<ChatComposer
input={input}
setInput={setInput}
sendMessage={handleSend}
placeholder={t("placeholder")}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
supportsThinking={supportsThinking}
thinkingEnabled={thinkingEnabled}
setThinkingEnabled={setThinkingEnabled}
large
/>
<Button
variant="select"
className="size-10 shrink-0 rounded-full"
disabled={!input.trim()}
onClick={handleSubmit}
>
<FaArrowUpLong size="18" />
</Button>
</div>
</div>
);
+42 -28
View File
@@ -8,6 +8,12 @@ import {
} from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type ReasoningBubbleProps = {
/** The accumulated reasoning text from the model. */
@@ -54,34 +60,42 @@ export function ReasoningBubble({
return (
<div className="self-start rounded-2xl bg-muted/60 px-3 py-2 text-muted-foreground">
<Collapsible open={open} onOpenChange={handleOpenChange}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-auto w-full min-w-0 justify-start gap-2 whitespace-normal p-0 text-left text-xs hover:bg-transparent"
>
<LuBrain
className={cn(
"size-3 shrink-0",
!answerStarted && "animate-pulse",
)}
/>
<span className="break-words font-medium">{label}</span>
{answerStarted &&
(open ? (
<LuChevronDown className="ml-auto size-3 shrink-0" />
) : (
<LuChevronRight className="ml-auto size-3 shrink-0" />
))}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<pre className="scrollbar-container mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-2 font-sans text-xs leading-relaxed">
{reasoning}
</pre>
</CollapsibleContent>
</Collapsible>
<TooltipProvider>
<Collapsible open={open} onOpenChange={handleOpenChange}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-auto w-full min-w-0 justify-start gap-2 whitespace-normal p-0 text-left text-xs hover:bg-transparent"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<LuBrain
className={cn(
"size-3 shrink-0",
!answerStarted && "animate-pulse",
)}
/>
</div>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
{answerStarted &&
(open ? (
<LuChevronDown className="ml-auto size-3 shrink-0" />
) : (
<LuChevronRight className="ml-auto size-3 shrink-0" />
))}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<pre className="scrollbar-container mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/50 p-2 font-sans text-xs leading-relaxed">
{reasoning}
</pre>
</CollapsibleContent>
</Collapsible>
</TooltipProvider>
</div>
);
}
@@ -23,6 +23,7 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import type { ConfigFormContext, JsonObject } from "@/types/configForm";
import type { GenAIModelsResponse } from "@/types/chat";
import { getSizedFieldClassName } from "../utils";
type ProbeResponse =
@@ -73,11 +74,12 @@ export function GenAIModelWidget(props: WidgetProps) {
return `${e.provider ?? ""}|${e.base_url ?? ""}`;
}, [providerKey, formContext?.fullConfig]);
const { data: allModels, mutate: mutateModels } = useSWR<
Record<string, string[]>
>("genai/models", {
revalidateOnFocus: false,
});
const { data: allModels, mutate: mutateModels } = useSWR<GenAIModelsResponse>(
"genai/models",
{
revalidateOnFocus: false,
},
);
// Revalidate models when the saved config fingerprint changes (e.g. after
// switching provider or base_url and saving).
@@ -89,9 +91,9 @@ export function GenAIModelWidget(props: WidgetProps) {
}
}, [configFingerprint, mutateModels]);
const fetchedModels = useMemo(() => {
const fetchedModels = useMemo<string[]>(() => {
if (!allModels || !providerKey) return [];
return allModels[providerKey] ?? [];
return allModels[providerKey]?.models ?? [];
}, [allModels, providerKey]);
const [probeStatus, setProbeStatus] = useState<ProbeStatus>("idle");