feat(ui): replace collapsible config summary bar with always-visible tool chips
Remove the "Auto · 云费用 1" summary toggle button that occupied a dedicated line below the textarea. Model mode and tool group chips are now displayed inline and always visible, eliminating the redundant status row while keeping all selection controls accessible. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3d723e5705
commit
b59f672033
@@ -1,6 +1,6 @@
|
||||
import { type RefObject } from "react";
|
||||
import { Square, Loader2 } from "lucide-react";
|
||||
import { QUICK_PROMPTS, type ToolKey } from "@/utils/tool-maps";
|
||||
import { QUICK_PROMPTS } from "@/utils/tool-maps";
|
||||
import FileUploadButton, { type SelectedFile } from "@/components/FileUploadButton.tsx";
|
||||
import FileAttachmentPreview from "@/components/FileAttachmentPreview.tsx";
|
||||
|
||||
@@ -16,7 +16,6 @@ interface ChatInputProps {
|
||||
setSourceLabel: (v: string | null) => void;
|
||||
concurrentStatus: "idle" | "generating" | "stopping" | "cancelling";
|
||||
setConcurrentStatus: (s: "idle" | "generating" | "stopping" | "cancelling") => void;
|
||||
activeTools: Set<ToolKey>;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onStop: () => void;
|
||||
onPaste: (e: React.ClipboardEvent) => void;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { type RefObject } from "react";
|
||||
import { ChevronDown, Sparkles } from "lucide-react";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TOOL_GROUPS, type ToolKey, type ModelMode, MODEL_OPTIONS } from "@/utils/tool-maps";
|
||||
import { ExportButton } from "@/components/shared/ExportButton";
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
interface ConfigPanelProps {
|
||||
configOpen: boolean;
|
||||
setConfigOpen: (open: boolean | ((prev: boolean) => boolean)) => void;
|
||||
configPanelRef: RefObject<HTMLDivElement | null>;
|
||||
modelMode: ModelMode;
|
||||
setModelMode: (mode: ModelMode) => void;
|
||||
@@ -18,8 +16,6 @@ interface ConfigPanelProps {
|
||||
}
|
||||
|
||||
export function ConfigPanel({
|
||||
configOpen,
|
||||
setConfigOpen,
|
||||
configPanelRef,
|
||||
modelMode,
|
||||
setModelMode,
|
||||
@@ -28,113 +24,74 @@ export function ConfigPanel({
|
||||
toggleTool,
|
||||
activeMessages,
|
||||
}: ConfigPanelProps) {
|
||||
const modelLabel = modelMode === "flash" ? "Flash" : modelMode === "pro" ? "Pro" : "Auto";
|
||||
const activeToolLabels = TOOL_GROUPS
|
||||
.filter((g) => activeTools.has(g.key))
|
||||
.map((g) => g.label);
|
||||
const summary =
|
||||
activeToolLabels.length === 0
|
||||
? modelLabel
|
||||
: activeToolLabels.length <= 2
|
||||
? `${modelLabel} · ${activeToolLabels.join(" + ")}`
|
||||
: `${modelLabel} · ${activeToolLabels.length} 个工具`;
|
||||
|
||||
return (
|
||||
<div className="shrink-0 border-t border-border px-4 pt-2 pb-0 max-w-3xl mx-auto w-full">
|
||||
<div ref={configPanelRef} className="relative flex items-center gap-2">
|
||||
<div ref={configPanelRef} className="flex items-center gap-3 flex-wrap">
|
||||
{/* Export button — hidden when no messages */}
|
||||
{activeMessages.length > 0 && (
|
||||
<ExportButton messages={activeMessages as Array<Record<string, unknown>>} />
|
||||
)}
|
||||
{/* Summary bar */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfigOpen((o) => !o)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 text-xs transition-colors py-1 px-2 rounded-md",
|
||||
configOpen
|
||||
? "text-foreground bg-accent"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<ChevronDown className={cn("size-3.5 transition-transform duration-200", configOpen && "rotate-180")} />
|
||||
<span>{summary}</span>
|
||||
{activeTools.size > 0 && (
|
||||
<span className="inline-flex items-center justify-center size-4 rounded-full bg-primary text-primary-foreground text-[10px] font-medium leading-none">
|
||||
{activeTools.size}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Expanded panel — opens upward */}
|
||||
{configOpen && (
|
||||
<div className="absolute bottom-full mb-1 left-0 z-20 border border-border rounded-xl bg-background shadow-sm p-3 flex flex-col gap-3 min-w-[320px] animate-in fade-in slide-in-from-bottom-2 duration-150">
|
||||
{/* Model mode row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground w-14 shrink-0">模型模式</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{MODEL_OPTIONS.map(({ value, label, icon: Icon }) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setModelMode(value)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
|
||||
modelMode === value
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Model mode chips */}
|
||||
<div className="flex items-center gap-1">
|
||||
{MODEL_OPTIONS.map(({ value, label, icon: Icon }) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setModelMode(value)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
|
||||
modelMode === value
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tool toggles row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground w-14 shrink-0">工具</span>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{/* Auto chip */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTools(new Set())}
|
||||
title="自动模式:由 AI 决定使用哪些工具"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
|
||||
activeTools.size === 0
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Sparkles className="size-3" />
|
||||
自动
|
||||
</button>
|
||||
{TOOL_GROUPS.map(({ key, label, icon: Icon }) => {
|
||||
const isOn = activeTools.has(key);
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => toggleTool(key)}
|
||||
title={isOn ? `${label}:已启用` : `${label}:已禁用`}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
|
||||
isOn
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Divider */}
|
||||
<div className="w-px h-3.5 bg-border shrink-0" />
|
||||
|
||||
{/* Tool chips */}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTools(new Set())}
|
||||
title="自动模式:由 AI 决定使用哪些工具"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
|
||||
activeTools.size === 0
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Sparkles className="size-3" />
|
||||
自动
|
||||
</button>
|
||||
{TOOL_GROUPS.map(({ key, label, icon: Icon }) => {
|
||||
const isOn = activeTools.has(key);
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => toggleTool(key)}
|
||||
title={isOn ? `${label}:已启用` : `${label}:已禁用`}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
|
||||
isOn
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+4
-12
@@ -12,7 +12,7 @@ import CanvasPanel, { type CanvasDoc } from "@/components/CanvasPanel.tsx";
|
||||
import { type SelectedFile } from "@/components/FileUploadButton.tsx";
|
||||
import { TOOL_GROUPS } from "@/utils/tool-maps";
|
||||
import { deduplicateMessages, type UIMsgLocal } from "@/utils/thread-management";
|
||||
import { setupGlobalKeyboardShortcuts, autoResizeTextarea, setupConfigPanelEscapeClose, setupPrefillInputListener, setupRetryToolListener } from "@/utils/input-handling";
|
||||
import { setupGlobalKeyboardShortcuts, autoResizeTextarea, setupPrefillInputListener, setupRetryToolListener } from "@/utils/input-handling";
|
||||
import { createLangGraphClient } from "@/utils/api-client";
|
||||
import { remoteLog } from "@/utils/remote-log";
|
||||
import { useConversation } from "@/hooks/useConversation";
|
||||
@@ -34,7 +34,7 @@ function App() {
|
||||
const [showScrollBtn, setShowScrollBtn] = useState(false);
|
||||
const isComposingRef = useRef(false);
|
||||
|
||||
const { activeTools, setActiveTools, modelMode, setModelMode, configOpen, setConfigOpen, toggleTool } = useConfig();
|
||||
const { activeTools, setActiveTools, modelMode, setModelMode, toggleTool } = useConfig();
|
||||
|
||||
const {
|
||||
threads, setThreads,
|
||||
@@ -132,11 +132,6 @@ function App() {
|
||||
setTimeout(() => textareaRef.current?.focus(), 50);
|
||||
}), []);
|
||||
|
||||
// Close config panel when clicking outside or pressing Escape
|
||||
useEffect(() => {
|
||||
if (!configOpen) return;
|
||||
return setupConfigPanelEscapeClose(configPanelRef, { onClose: () => setConfigOpen(false) });
|
||||
}, [configOpen]);
|
||||
|
||||
// Global keyboard shortcuts
|
||||
useEffect(() => setupGlobalKeyboardShortcuts({
|
||||
@@ -411,10 +406,8 @@ function App() {
|
||||
resetLoading={resetLoading}
|
||||
/>
|
||||
|
||||
{/* Config summary bar */}
|
||||
{/* Config panel */}
|
||||
<ConfigPanel
|
||||
configOpen={configOpen}
|
||||
setConfigOpen={setConfigOpen}
|
||||
configPanelRef={configPanelRef}
|
||||
modelMode={modelMode}
|
||||
setModelMode={setModelMode}
|
||||
@@ -458,11 +451,10 @@ function App() {
|
||||
setSourceLabel={setSourceLabel}
|
||||
concurrentStatus={concurrentStatus}
|
||||
setConcurrentStatus={setConcurrentStatus}
|
||||
activeTools={activeTools}
|
||||
onSubmit={handleSubmit}
|
||||
onStop={handleStop}
|
||||
onPaste={handlePaste}
|
||||
showQuickPrompts={activeMessages.length === 0 && !configOpen}
|
||||
showQuickPrompts={activeMessages.length === 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user