feat: collapse model/tool config into expandable summary bar
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 17s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 43s

Replace flat tool toggles + model selector bar with a default-collapsed
summary chip (e.g. "Auto · 已开启 3 个工具 ▾") that expands upward into
a compact panel. Panel auto-closes on outside mousedown click. Options
changes keep the panel open.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-12 14:40:18 +08:00
co-authored by Claude Sonnet 4.6
parent d86da0e48b
commit 7167109414
+105 -59
View File
@@ -10,7 +10,6 @@ import { useState, useRef, useEffect, useCallback } from "react";
import ComponentMap from "./agent-uis/index.tsx";
import "./index.css";
import { BookOpen, Search, Terminal, Ticket, Zap, Cpu, Bot, Menu, Copy, Check, RefreshCw, Square, Pencil, ChevronDown, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ThreadSidebar, type ThreadItem, updateThreadLastActive } from "@/components/ThreadSidebar.tsx";
import { ThemeProvider } from "next-themes";
@@ -142,6 +141,10 @@ function App() {
// Source label from card action buttons (e.g. "来自 知识库检索")
const [sourceLabel, setSourceLabel] = useState<string | null>(null);
// Config panel open/close state
const [configOpen, setConfigOpen] = useState(false);
const configPanelRef = useRef<HTMLDivElement>(null);
// Pending retry tool name — set by soc:retry-tool, consumed on next submit
const pendingRetryToolRef = useRef<string | null>(null);
@@ -208,6 +211,18 @@ function App() {
return () => window.removeEventListener("soc:prefill-input", handler);
}, []);
// Close config panel when clicking outside
useEffect(() => {
if (!configOpen) return;
function handleMouseDown(e: MouseEvent) {
if (configPanelRef.current && !configPanelRef.current.contains(e.target as Node)) {
setConfigOpen(false);
}
}
document.addEventListener("mousedown", handleMouseDown);
return () => document.removeEventListener("mousedown", handleMouseDown);
}, [configOpen]);
// Load threads on mount
useEffect(() => {
client.threads
@@ -798,70 +813,101 @@ function App() {
)}
</div>
{/* Control bar: tools (left) + model selector (right) */}
<div className="shrink-0 border-t border-border px-4 pt-3 pb-0 flex items-center justify-between max-w-3xl mx-auto w-full">
{/* Tool toggles */}
<div className="flex items-center gap-1.5">
{/* Auto chip — active when no tools are selected */}
<button
type="button"
onClick={() => setActiveTools(new Set())}
title="自动模式:由 AI 决定使用哪些工具"
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 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.5" />
自动
</button>
{TOOL_GROUPS.map(({ key, label, icon: Icon }) => {
const isOn = activeTools.has(key);
return (
{/* Config summary bar + expandable panel */}
{(() => {
const modelLabel = modelMode === "flash" ? "Flash" : modelMode === "pro" ? "Pro" : "Auto";
const toolCount = activeTools.size;
const summary = toolCount > 0 ? `${modelLabel} · 已开启 ${toolCount} 个工具` : modelLabel;
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">
{/* Summary bar */}
<button
key={key}
type="button"
onClick={() => toggleTool(key)}
title={isOn ? `${label}:已启用` : `${label}:已禁用`}
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors",
isOn
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground",
)}
onClick={() => setConfigOpen((o) => !o)}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors py-1"
>
<Icon className="size-3.5" />
{label}
{configOpen ? (
<ChevronDown className="size-3.5 rotate-180 transition-transform" />
) : (
<ChevronDown className="size-3.5 transition-transform" />
)}
{summary}
</button>
);
})}
</div>
{/* Model selector */}
<div className="flex items-center gap-0.5">
{MODEL_OPTIONS.map(({ value, label, icon: Icon }) => (
<Button
key={value}
type="button"
variant={modelMode === value ? "default" : "ghost"}
size="sm"
className={cn(
"h-7 px-2.5 text-xs gap-1",
value === "auto" && modelMode !== value && "font-semibold text-primary",
{/* 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]">
{/* 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>
{/* 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>
)}
onClick={() => setModelMode(value)}
>
<Icon className="size-3.5" />
{label}
{value === "auto" && modelMode !== value && (
<span className="ml-0.5 text-[9px] text-primary/70">推荐</span>
)}
</Button>
))}
</div>
</div>
</div>
</div>
);
})()}
{/* Input */}
<div className="shrink-0 px-4 py-3">