feat: collapse model/tool config into expandable summary bar
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:
co-authored by
Claude Sonnet 4.6
parent
d86da0e48b
commit
7167109414
+105
-59
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user