feat(ui): replace collapsible config summary bar with always-visible tool chips
Trigger auto deployment for soc-langgraph / build-and-deploy (push) Failing after 14s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 46s

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:
gongzhiyong
2026-04-15 01:29:31 +08:00
co-authored by Claude Sonnet 4.6
parent 3d723e5705
commit b59f672033
3 changed files with 66 additions and 118 deletions
+1 -2
View File
@@ -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;
+61 -104
View File
@@ -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
View File
@@ -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>