feat: put all options into '+'

This commit is contained in:
Henry Li
2026-01-22 13:43:45 +08:00
parent 31bf49917c
commit 7d4d706738
7 changed files with 197 additions and 141 deletions

View File

@@ -30,12 +30,7 @@ export default async function RootLayout({
suppressHydrationWarning suppressHydrationWarning
> >
<body> <body>
<ThemeProvider <ThemeProvider attribute="class" enableSystem disableTransitionOnChange>
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
<I18nProvider initialLocale={locale}>{children}</I18nProvider> <I18nProvider initialLocale={locale}>{children}</I18nProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>

View File

@@ -147,21 +147,12 @@ export default function ChatPage() {
<div <div
className={cn( className={cn(
"relative w-full", "relative w-full",
isNewThread && "-translate-y-[calc(50vh-120px)]", isNewThread && "-translate-y-[calc(50vh-160px)]",
isNewThread isNewThread
? "max-w-(--container-width-sm)" ? "max-w-(--container-width-sm)"
: "max-w-(--container-width-md)", : "max-w-(--container-width-md)",
)} )}
> >
{isNewThread && (
<div
className={cn(
"absolute right-0 bottom-[136px] left-0 flex",
)}
>
<Welcome />
</div>
)}
<div className="absolute -top-4 right-0 left-0 z-0"> <div className="absolute -top-4 right-0 left-0 z-0">
<div className="absolute right-0 bottom-0 left-0"> <div className="absolute right-0 bottom-0 left-0">
<TodoList <TodoList
@@ -183,6 +174,7 @@ export default function ChatPage() {
autoFocus={isNewThread} autoFocus={isNewThread}
status={thread.isLoading ? "streaming" : "ready"} status={thread.isLoading ? "streaming" : "ready"}
context={settings.context} context={settings.context}
extraHeader={isNewThread && <Welcome />}
onContextChange={(context) => onContextChange={(context) =>
setSettings("context", context) setSettings("context", context)
} }

View File

@@ -44,6 +44,7 @@ import {
PaperclipIcon, PaperclipIcon,
PlusIcon, PlusIcon,
SquareIcon, SquareIcon,
UploadIcon,
XIcon, XIcon,
} from "lucide-react"; } from "lucide-react";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@@ -422,7 +423,7 @@ export const PromptInputActionAddAttachments = ({
attachments.openFileDialog(); attachments.openFileDialog();
}} }}
> >
<ImageIcon className="mr-2 size-4" /> {label} <PaperclipIcon className="mr-2 size-4" /> {label}
</DropdownMenuItem> </DropdownMenuItem>
); );
}; };

View File

@@ -1,18 +1,36 @@
"use client"; "use client";
import type { ChatStatus } from "ai"; import type { ChatStatus } from "ai";
import { CheckIcon, LightbulbIcon, ListTodoIcon } from "lucide-react"; import {
CheckIcon,
GraduationCapIcon,
LightbulbIcon,
ZapIcon,
} from "lucide-react";
import { useCallback, useMemo, useState, type ComponentProps } from "react"; import { useCallback, useMemo, useState, type ComponentProps } from "react";
import { import {
PromptInput, PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuItem,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody, PromptInputBody,
PromptInputButton, PromptInputButton,
PromptInputFooter, PromptInputFooter,
PromptInputSubmit, PromptInputSubmit,
PromptInputTextarea, PromptInputTextarea,
PromptInputTools,
type PromptInputMessage, type PromptInputMessage,
} from "@/components/ai-elements/prompt-input"; } from "@/components/ai-elements/prompt-input";
import {
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
import type { AgentThreadContext } from "@/core/threads"; import type { AgentThreadContext } from "@/core/threads";
@@ -28,13 +46,12 @@ import {
ModelSelectorTrigger, ModelSelectorTrigger,
} from "../ai-elements/model-selector"; } from "../ai-elements/model-selector";
import { Tooltip } from "./tooltip";
export function InputBox({ export function InputBox({
className, className,
autoFocus, autoFocus,
status = "ready", status = "ready",
context, context,
extraHeader,
onContextChange, onContextChange,
onSubmit, onSubmit,
onStop, onStop,
@@ -55,6 +72,19 @@ export function InputBox({
() => models.find((m) => m.name === context.model_name), () => models.find((m) => m.name === context.model_name),
[context.model_name, models], [context.model_name, models],
); );
const supportThinking = useMemo(
() => selectedModel?.supports_thinking ?? false,
[selectedModel],
);
const mode = useMemo(() => {
if (context.is_plan_mode) {
return "pro";
}
if (context.thinking_enabled) {
return "thinking";
}
return "flash";
}, [context.thinking_enabled, context.is_plan_mode]);
const handleModelSelect = useCallback( const handleModelSelect = useCallback(
(model_name: string) => { (model_name: string) => {
const supports_thinking = selectedModel?.supports_thinking ?? false; const supports_thinking = selectedModel?.supports_thinking ?? false;
@@ -67,18 +97,30 @@ export function InputBox({
}, },
[selectedModel?.supports_thinking, onContextChange, context], [selectedModel?.supports_thinking, onContextChange, context],
); );
const handleThinkingToggle = useCallback(() => { const handleModeSelect = useCallback(
onContextChange?.({ (mode: "flash" | "thinking" | "pro") => {
...context, if (mode === "flash") {
thinking_enabled: !context.thinking_enabled, onContextChange?.({
}); ...context,
}, [onContextChange, context]); thinking_enabled: false,
const handlePlanModeToggle = useCallback(() => { is_plan_mode: false,
onContextChange?.({ });
...context, } else if (mode === "thinking") {
is_plan_mode: !context.is_plan_mode, onContextChange?.({
}); ...context,
}, [onContextChange, context]); thinking_enabled: true,
is_plan_mode: false,
});
} else if (mode === "pro") {
onContextChange?.({
...context,
thinking_enabled: true,
is_plan_mode: true,
});
}
},
[onContextChange, context],
);
const handleSubmit = useCallback( const handleSubmit = useCallback(
async (message: PromptInputMessage) => { async (message: PromptInputMessage) => {
if (status === "streaming") { if (status === "streaming") {
@@ -103,6 +145,16 @@ export function InputBox({
onSubmit={handleSubmit} onSubmit={handleSubmit}
{...props} {...props}
> >
{extraHeader && (
<div className="absolute top-0 right-0 left-0 z-10">
<div className="absolute right-0 bottom-0 left-0 flex items-center justify-center">
{extraHeader}
</div>
</div>
)}
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
<PromptInputBody className="absolute top-0 right-0 left-0 z-3"> <PromptInputBody className="absolute top-0 right-0 left-0 z-3">
<PromptInputTextarea <PromptInputTextarea
className={cn("size-full")} className={cn("size-full")}
@@ -111,91 +163,111 @@ export function InputBox({
/> />
</PromptInputBody> </PromptInputBody>
<PromptInputFooter className="flex"> <PromptInputFooter className="flex">
<div className="flex items-center"> <PromptInputTools>
<Tooltip <PromptInputActionMenu>
content={ <PromptInputActionMenuTrigger />
context.thinking_enabled ? ( <PromptInputActionMenuContent className="w-80">
<div className="tex-sm flex flex-col gap-1"> <PromptInputActionAddAttachments
<div>{t.inputBox.thinkingEnabled}</div> label={t.inputBox.addAttachments}
<div className="opacity-50"> />
{t.inputBox.clickToDisableThinking} <DropdownMenuSeparator />
</div> <DropdownMenuGroup>
</div> <DropdownMenuLabel className="text-muted-foreground text-xs">
) : ( {t.inputBox.mode}
<div className="tex-sm flex flex-col gap-1"> </DropdownMenuLabel>
<div>{t.inputBox.thinkingDisabled}</div> <PromptInputActionMenu>
<div className="opacity-50"> <PromptInputActionMenuItem
{t.inputBox.clickToEnableThinking}
</div>
</div>
)
}
>
{selectedModel?.supports_thinking && (
<PromptInputButton onClick={handleThinkingToggle}>
<>
{context.thinking_enabled ? (
<LightbulbIcon className="text-primary size-4" />
) : (
<LightbulbIcon className="size-4" />
)}
<span
className={cn( className={cn(
"text-xs font-normal", mode === "flash"
context.thinking_enabled ? "text-accent-foreground"
? "text-primary" : "text-muted-foreground/65",
: "text-muted-foreground",
)} )}
onSelect={() => handleModeSelect("flash")}
> >
{t.inputBox.thinking} <div className="flex flex-col gap-2">
</span> <div className="flex items-center gap-1 font-bold">
</> <ZapIcon
</PromptInputButton> className={cn(
)} "mr-2 size-4",
</Tooltip> mode === "flash" && "text-accent-foreground",
<Tooltip )}
content={ />
context.is_plan_mode ? ( {t.inputBox.flashMode}
<div className="tex-sm flex flex-col gap-1"> </div>
<div>{t.inputBox.planMode}</div> <div className="pl-7 text-xs">
<div className="opacity-50"> {t.inputBox.flashModeDescription}
{t.inputBox.clickToDisablePlanMode} </div>
</div> </div>
</div> {mode === "flash" ? (
) : ( <CheckIcon className="ml-auto size-4" />
<div className="tex-sm flex flex-col gap-1"> ) : (
<div>{t.inputBox.planMode}</div> <div className="ml-auto size-4" />
<div className="opacity-50"> )}
{t.inputBox.clickToEnablePlanMode} </PromptInputActionMenuItem>
</div> {supportThinking && (
</div> <PromptInputActionMenuItem
) className={cn(
} mode === "thinking"
> ? "text-accent-foreground"
{selectedModel?.supports_thinking && ( : "text-muted-foreground/65",
<PromptInputButton onClick={handlePlanModeToggle}> )}
<> onSelect={() => handleModeSelect("thinking")}
{context.is_plan_mode ? ( >
<ListTodoIcon className="text-primary size-4" /> <div className="flex flex-col gap-2">
) : ( <div className="flex items-center gap-1 font-bold">
<ListTodoIcon className="size-4" /> <LightbulbIcon
className={cn(
"mr-2 size-4",
mode === "thinking" && "text-accent-foreground",
)}
/>
{t.inputBox.reasoningMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.reasoningModeDescription}
</div>
</div>
{mode === "thinking" ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
)} )}
<span <PromptInputActionMenuItem
className={cn( className={cn(
"text-xs font-normal", mode === "pro"
context.is_plan_mode ? "text-accent-foreground"
? "text-primary" : "text-muted-foreground/65",
: "text-muted-foreground",
)} )}
onSelect={() => handleModeSelect("pro")}
> >
{t.inputBox.planMode} <div className="flex flex-col gap-2">
</span> <div className="flex items-center gap-1 font-bold">
</> <GraduationCapIcon
</PromptInputButton> className={cn(
)} "mr-2 size-4",
</Tooltip> mode === "pro" && "text-accent-foreground",
</div> )}
<div className="flex items-center gap-2"> />
{t.inputBox.proMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.proModeDescription}
</div>
</div>
{mode === "pro" ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
</PromptInputActionMenu>
</DropdownMenuGroup>
</PromptInputActionMenuContent>
</PromptInputActionMenu>
</PromptInputTools>
<PromptInputTools>
<ModelSelector <ModelSelector
open={modelDialogOpen} open={modelDialogOpen}
onOpenChange={setModelDialogOpen} onOpenChange={setModelDialogOpen}
@@ -232,7 +304,7 @@ export function InputBox({
variant="outline" variant="outline"
status={status} status={status}
/> />
</div> </PromptInputTools>
</PromptInputFooter> </PromptInputFooter>
</PromptInput> </PromptInput>
); );

View File

@@ -39,16 +39,16 @@ export const enUS: Translations = {
// Input Box // Input Box
inputBox: { inputBox: {
placeholder: "How can I assist you today?", placeholder: "How can I assist you today?",
thinking: "Thinking", addAttachments: "Add attachments",
thinkingEnabled: "Thinking is enabled", mode: "Mode",
thinkingDisabled: "Thinking is disabled", flashMode: "Flash",
clickToDisableThinking: "Click to disable thinking", flashModeDescription: "Fast and efficient",
clickToEnableThinking: "Click to enable thinking", reasoningMode: "Reasoning",
planMode: "Plan mode", reasoningModeDescription:
planModeEnabled: "Plan mode is enabled", "Reasoning before action, balance between time and accuracy",
planModeDisabled: "Plan mode is disabled", proMode: "Pro",
clickToDisablePlanMode: "Click to disable plan mode", proModeDescription:
clickToEnablePlanMode: "Click to enable plan mode", "Reasoning, planning and executing, get more accurate results, may take more time",
searchModels: "Search models...", searchModels: "Search models...",
}, },

View File

@@ -36,16 +36,14 @@ export interface Translations {
// Input Box // Input Box
inputBox: { inputBox: {
placeholder: string; placeholder: string;
thinking: string; addAttachments: string;
thinkingEnabled: string; mode: string;
thinkingDisabled: string; flashMode: string;
clickToDisableThinking: string; flashModeDescription: string;
clickToEnableThinking: string; reasoningMode: string;
planMode: string; reasoningModeDescription: string;
planModeEnabled: string; proMode: string;
planModeDisabled: string; proModeDescription: string;
clickToDisablePlanMode: string;
clickToEnablePlanMode: string;
searchModels: string; searchModels: string;
}; };

View File

@@ -39,16 +39,14 @@ export const zhCN: Translations = {
// Input Box // Input Box
inputBox: { inputBox: {
placeholder: "今天我能为你做些什么?", placeholder: "今天我能为你做些什么?",
thinking: "思考", addAttachments: "添加附件",
thinkingEnabled: "思考功能已启用", mode: "模式",
thinkingDisabled: "思考功能已禁用", flashMode: "闪速",
clickToDisableThinking: "点击禁用思考功能", flashModeDescription: "快速且高效的完成任务,但可能不够精准",
clickToEnableThinking: "点击启用思考功能", reasoningMode: "思考",
planMode: "To-do 模式", reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
planModeEnabled: "To-do 模式已启用", proMode: "专业",
planModeDisabled: "To-do 模式已禁用", proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
clickToDisablePlanMode: "点击禁用 To-do 模式",
clickToEnablePlanMode: "点击启用 To-do 模式",
searchModels: "搜索模型...", searchModels: "搜索模型...",
}, },