refactor(ui): refine glass surfaces
This commit is contained in:
@@ -4,7 +4,6 @@ import { ChevronRight } from "lucide-react";
|
||||
import type { PersonaState } from "@/shared/ai-elements/persona";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
MAP_CONTROL_SURFACE_CLASS_NAME,
|
||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
|
||||
} from "@/features/map/core/components/map-control-styles";
|
||||
import { AgentPersona } from "./agent-persona";
|
||||
@@ -16,64 +15,25 @@ type AgentCollapsedRailProps = {
|
||||
};
|
||||
|
||||
export function AgentCollapsedRail({ personaState, statusLabel = "Agent", onExpand }: AgentCollapsedRailProps) {
|
||||
const state = getCollapsedAgentState(statusLabel, personaState);
|
||||
|
||||
return (
|
||||
<aside className={cn("agent-rail-enter pointer-events-auto w-[188px] p-2", MAP_MAJOR_PANEL_RADIUS_CLASS_NAME, MAP_CONTROL_SURFACE_CLASS_NAME)}>
|
||||
<div className="agent-panel-control agent-rail-item rounded-2xl p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentPersona
|
||||
className="h-10 w-10"
|
||||
fallbackClassName="h-10 w-10"
|
||||
state={personaState}
|
||||
statusLabel={statusLabel}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-semibold text-slate-950">HydroAgent</p>
|
||||
<p className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs font-semibold text-slate-500">
|
||||
<span className={cn("h-1.5 w-1.5 shrink-0 rounded-full", state.dotClassName)} />
|
||||
<span className="truncate" title={statusLabel}>{state.label}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="展开 Agent 面板"
|
||||
title="展开 Agent 面板"
|
||||
onClick={onExpand}
|
||||
className="agent-panel-primary-icon grid h-9 w-9 shrink-0 place-items-center rounded-xl transition"
|
||||
>
|
||||
<ChevronRight size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<aside className={cn("agent-rail-enter glass-control pointer-events-auto w-[136px] p-1.5 shadow-[0_6px_18px_rgba(15,23,42,0.12)]", MAP_MAJOR_PANEL_RADIUS_CLASS_NAME)}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="展开排水助手面板"
|
||||
title="展开排水助手面板"
|
||||
onClick={onExpand}
|
||||
className="agent-rail-item group flex h-14 w-full items-center justify-center gap-4 rounded-xl bg-white px-3 transition hover:bg-slate-50"
|
||||
>
|
||||
<AgentPersona
|
||||
className="h-12 w-12"
|
||||
fallbackClassName="h-12 w-12"
|
||||
state={personaState}
|
||||
statusLabel={statusLabel}
|
||||
/>
|
||||
<span className="agent-panel-primary-icon grid h-8 w-8 shrink-0 place-items-center rounded-lg !border-0 transition group-hover:translate-x-0.5">
|
||||
<ChevronRight size={17} strokeWidth={2.25} aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function getCollapsedAgentState(statusLabel: string, personaState?: PersonaState) {
|
||||
if (personaState === "asleep" || /失败|不可用|错误|降级/.test(statusLabel)) {
|
||||
return {
|
||||
label: "离线",
|
||||
dotClassName: "bg-red-500"
|
||||
};
|
||||
}
|
||||
|
||||
if (personaState === "listening") {
|
||||
return {
|
||||
label: "待确认",
|
||||
dotClassName: "bg-orange-500"
|
||||
};
|
||||
}
|
||||
|
||||
if (personaState === "thinking") {
|
||||
return {
|
||||
label: "处理中",
|
||||
dotClassName: "bg-blue-500"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: "在线",
|
||||
dotClassName: "bg-emerald-500"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bolt,
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
ChevronsUpDown,
|
||||
ChevronLeft,
|
||||
Gauge,
|
||||
DatabaseZap,
|
||||
GitMerge,
|
||||
History,
|
||||
MapPinned,
|
||||
Plus,
|
||||
SendHorizontal,
|
||||
Sparkles
|
||||
SendHorizontal
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, MotionConfig, motion } from "motion/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
MAP_CONTROL_SURFACE_CLASS_NAME,
|
||||
MAP_TOOL_PANEL_SURFACE_CLASS_NAME,
|
||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
|
||||
} from "@/features/map/core/components/map-control-styles";
|
||||
import type { AgentChatSessionSummary } from "../api/client";
|
||||
@@ -100,6 +101,13 @@ const AGENT_PROMPT_SUGGESTIONS = [
|
||||
"当前管网哪些区域可能存在雨污混接,判断依据是什么?"
|
||||
] as const;
|
||||
|
||||
const AGENT_CAPABILITIES = [
|
||||
{ icon: DatabaseZap, label: "监测质量诊断" },
|
||||
{ icon: Activity, label: "异常状态研判" },
|
||||
{ icon: GitMerge, label: "雨污混接识别" },
|
||||
{ icon: MapPinned, label: "GIS 地图联动" }
|
||||
] as const;
|
||||
|
||||
export function AgentCommandPanel({
|
||||
collapsing = false,
|
||||
personaState,
|
||||
@@ -155,13 +163,14 @@ export function AgentCommandPanel({
|
||||
<aside
|
||||
className={cn(
|
||||
"pointer-events-auto flex h-full min-h-0 w-full flex-col overflow-hidden",
|
||||
"agent-panel-shell",
|
||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
||||
MAP_CONTROL_SURFACE_CLASS_NAME,
|
||||
MAP_TOOL_PANEL_SURFACE_CLASS_NAME,
|
||||
collapsing ? "agent-panel-collapse" : "agent-panel-enter"
|
||||
)}
|
||||
>
|
||||
<Agent className="flex h-full min-h-0 flex-col border-0 bg-transparent">
|
||||
<div className="agent-panel-band border-b border-white/70">
|
||||
<div className="agent-panel-band border-b border-slate-200">
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3 text-slate-900">
|
||||
<AgentPersona
|
||||
@@ -175,7 +184,7 @@ export function AgentCommandPanel({
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<p className="shrink-0 text-xs font-semibold uppercase text-slate-500">对话主题</p>
|
||||
<span className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-lg border border-slate-200/70 bg-[var(--glass-readable)] px-2 text-xs font-semibold text-slate-600">
|
||||
<span className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-lg border border-slate-200 bg-slate-50 px-2 text-xs font-semibold text-slate-600">
|
||||
<span className={cn("h-1.5 w-1.5 rounded-full", getAgentConnectionClassName(statusLabel))} />
|
||||
{getAgentConnectionLabel(statusLabel)}
|
||||
</span>
|
||||
@@ -273,7 +282,7 @@ export function AgentCommandPanel({
|
||||
|
||||
<AgentContent className="flex min-h-0 flex-1 flex-col gap-0 p-0">
|
||||
<Conversation className="agent-panel-conversation min-h-0">
|
||||
<ConversationContent className="gap-4 p-3">
|
||||
<ConversationContent className={cn("gap-4 p-3", messages.length === 0 && "min-h-full")}>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{messages.length > 0 ? (
|
||||
<BackendMessageList
|
||||
@@ -288,13 +297,17 @@ export function AgentCommandPanel({
|
||||
) : (
|
||||
<motion.div
|
||||
key="empty"
|
||||
className="w-full"
|
||||
className="flex flex-1 items-center justify-center px-5 py-10"
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={agentPanelItemVariants}
|
||||
>
|
||||
<AgentEmptyState statusLabel={statusLabel} />
|
||||
<AgentEmptyState
|
||||
personaState={personaState}
|
||||
statusLabel={statusLabel}
|
||||
streaming={streaming}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -312,22 +325,25 @@ export function AgentCommandPanel({
|
||||
</Conversation>
|
||||
</AgentContent>
|
||||
|
||||
<div className="agent-panel-band space-y-2 border-t border-white/70 p-3">
|
||||
<Suggestions aria-label="推荐问题" role="group" className="px-0.5">
|
||||
{AGENT_PROMPT_SUGGESTIONS.map((suggestion) => (
|
||||
<Suggestion
|
||||
key={suggestion}
|
||||
suggestion={suggestion}
|
||||
disabled={streaming}
|
||||
onClick={submitPrompt}
|
||||
className="max-w-full border-slate-200/80 bg-[var(--glass-readable)] text-slate-600 shadow-none hover:border-blue-200 hover:bg-blue-50 hover:text-blue-700"
|
||||
>
|
||||
<span className="truncate">{suggestion}</span>
|
||||
</Suggestion>
|
||||
))}
|
||||
</Suggestions>
|
||||
<div className="agent-panel-band relative border-t border-slate-200 p-3">
|
||||
{messages.length === 0 && !streaming ? (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-full z-10 bg-gradient-to-t from-white/95 via-white/80 to-transparent px-3 pb-2 pt-5">
|
||||
<Suggestions aria-label="推荐问题" role="group" className="pointer-events-auto px-0.5">
|
||||
{AGENT_PROMPT_SUGGESTIONS.map((suggestion) => (
|
||||
<Suggestion
|
||||
key={suggestion}
|
||||
suggestion={suggestion}
|
||||
onClick={submitPrompt}
|
||||
className="max-w-full border-slate-200 bg-white text-slate-600 shadow-sm hover:border-blue-200 hover:bg-blue-50 hover:text-blue-700"
|
||||
>
|
||||
<span className="truncate">{suggestion}</span>
|
||||
</Suggestion>
|
||||
))}
|
||||
</Suggestions>
|
||||
</div>
|
||||
) : null}
|
||||
<PromptInput
|
||||
className="agent-panel-control rounded-2xl shadow-sm"
|
||||
className="agent-panel-control overflow-hidden rounded-2xl shadow-sm transition-[border-color,box-shadow] has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring [&>[data-slot=input-group]]:rounded-[inherit] [&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:shadow-none [&>[data-slot=input-group]]:!ring-0"
|
||||
onSubmit={(message) => {
|
||||
const nextPrompt = message.text.trim();
|
||||
if (nextPrompt) {
|
||||
@@ -343,10 +359,7 @@ export function AgentCommandPanel({
|
||||
placeholder="输入调度问题,Agent 将通过后端会话流式响应"
|
||||
/>
|
||||
</PromptInputBody>
|
||||
<PromptInputFooter>
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-slate-500" title={statusLabel}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
<PromptInputFooter className="justify-end">
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<AgentModelSelect
|
||||
models={modelOptions}
|
||||
@@ -494,25 +507,25 @@ function AgentModelSelect({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-8 w-[126px] shrink-0 justify-between rounded-md border-transparent bg-transparent px-2 text-xs text-slate-600 shadow-none hover:bg-white/80"
|
||||
className="h-8 w-24 shrink-0 justify-between gap-1.5 rounded-md border-transparent bg-transparent px-1.5 text-xs text-slate-600 shadow-none hover:bg-slate-50"
|
||||
disabled={!onValueChange}
|
||||
aria-label="选择 Agent 模型"
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<ModelIcon model={selectedModel} size={13} />
|
||||
<span className="truncate">{selectedModel?.label ?? "模型"}</span>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-1">
|
||||
<ModelIcon model={selectedModel} size={selectedModel?.icon === "bolt" ? 15 : 14} />
|
||||
<span className="shrink-0">{selectedModel?.label ?? "模型"}</span>
|
||||
</span>
|
||||
<ChevronsUpDown size={13} className="shrink-0 opacity-50" aria-hidden="true" />
|
||||
<ChevronsUpDown size={12} className="shrink-0 opacity-50" aria-hidden="true" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="agent-panel-control w-72 rounded-xl p-2 shadow-lg"
|
||||
className="agent-panel-control w-[230px] rounded-xl p-2 shadow-lg"
|
||||
>
|
||||
<DropdownMenuLabel className="px-2 pb-2 pt-1 text-xs text-slate-500">
|
||||
Agent 模型
|
||||
模型选择
|
||||
</DropdownMenuLabel>
|
||||
<div className="space-y-1">
|
||||
{models.map((model) => {
|
||||
@@ -522,12 +535,12 @@ function AgentModelSelect({
|
||||
key={model.id}
|
||||
className={cn(
|
||||
"rounded-lg border border-transparent px-2.5 py-2 focus:bg-slate-50",
|
||||
selected && "glass-agent border-violet-200 focus:bg-violet-50"
|
||||
selected && "border-violet-200 bg-violet-50 focus:bg-violet-50"
|
||||
)}
|
||||
onSelect={() => onValueChange?.(model.id)}
|
||||
>
|
||||
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-white text-slate-600 shadow-sm">
|
||||
<ModelIcon model={model} size={15} />
|
||||
<span className="ml-0.5 shrink-0 pt-0.5">
|
||||
<ModelIcon model={model} size={model.icon === "bolt" ? 18 : 16} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-semibold text-slate-800">{model.label}</span>
|
||||
@@ -552,25 +565,86 @@ function getAgentConnectionClassName(statusLabel: string) {
|
||||
}
|
||||
|
||||
function ModelIcon({ model, size }: { model?: AgentModelOption; size: number }) {
|
||||
return model?.icon === "bolt" ? (
|
||||
<Bolt size={size} className="text-amber-500" aria-hidden="true" />
|
||||
) : (
|
||||
<Sparkles size={size} className="text-violet-500" aria-hidden="true" />
|
||||
return model?.icon === "bolt" ? <FastModelIcon size={size} /> : <ExpertModelIcon size={size} />;
|
||||
}
|
||||
|
||||
function FastModelIcon({ size }: { size: number }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="text-sky-600"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M10.67 21c-.35 0-.62-.31-.57-.66L11 14H7.5c-.88 0-.33-.75-.31-.78 1.26-2.23 3.15-5.53 5.65-9.93.1-.18.3-.29.5-.29.35 0 .62.31.57.66l-.9 6.34h3.51c.4 0 .62.19.4.66-3.29 5.74-5.2 9.09-5.75 10.05-.1.18-.29.29-.5.29"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentEmptyState({ statusLabel }: { statusLabel: string }) {
|
||||
function ExpertModelIcon({ size }: { size: number }) {
|
||||
return (
|
||||
<Message from="assistant" className="max-w-full">
|
||||
<MessageContent className="agent-panel-message w-full rounded-2xl p-3 text-sm leading-6 text-slate-600 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gauge size={15} className="text-blue-600" aria-hidden="true" />
|
||||
<p className="font-semibold text-slate-900">等待调度任务</p>
|
||||
</div>
|
||||
<p className="mt-2">后端会话已接入,新的任务会在这里形成证据、计划、预览和确认记录。</p>
|
||||
<p className="mt-1 text-xs text-slate-500">{statusLabel}</p>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="text-violet-600"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="m19.46 8 .79-1.75L22 5.46c.39-.18.39-.73 0-.91l-1.75-.79L19.46 2c-.18-.39-.73-.39-.91 0l-.79 1.75-1.76.79c-.39.18-.39.73 0 .91l1.75.79.79 1.76c.18.39.74.39.92 0M11.5 9.5 9.91 6c-.35-.78-1.47-.78-1.82 0L6.5 9.5 3 11.09c-.78.36-.78 1.47 0 1.82l3.5 1.59L8.09 18c.36.78 1.47.78 1.82 0l1.59-3.5 3.5-1.59c.78-.36.78-1.47 0-1.82zm7.04 6.5-.79 1.75-1.75.79c-.39.18-.39.73 0 .91l1.75.79.79 1.76c.18.39.73.39.91 0l.79-1.75 1.76-.79c.39-.18.39-.73 0-.91l-1.75-.79-.79-1.76c-.18-.39-.74-.39-.92 0"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentEmptyState({
|
||||
personaState,
|
||||
statusLabel,
|
||||
streaming
|
||||
}: {
|
||||
personaState?: PersonaState;
|
||||
statusLabel: string;
|
||||
streaming: boolean;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="agent-empty-state-title"
|
||||
className="mx-auto flex w-full max-w-sm flex-col items-center rounded-2xl border border-slate-200 bg-white p-4 text-center shadow-sm sm:p-5"
|
||||
>
|
||||
<AgentPersona
|
||||
className="mb-3 h-20 w-20"
|
||||
fallbackClassName="h-20 w-20 rounded-2xl"
|
||||
state={personaState}
|
||||
statusLabel={statusLabel}
|
||||
streaming={streaming}
|
||||
/>
|
||||
<p id="agent-empty-state-title" className="text-base font-semibold text-slate-900">
|
||||
从一个问题开始
|
||||
</p>
|
||||
<p className="mt-1.5 max-w-72 text-xs leading-5 text-slate-500">
|
||||
描述分析目标,或选择下方推荐问题。Agent 会整理证据并给出下一步建议。
|
||||
</p>
|
||||
<ul className="mt-4 grid w-full grid-cols-2 gap-2" aria-label="Agent 分析能力">
|
||||
{AGENT_CAPABILITIES.map(({ icon: Icon, label }) => (
|
||||
<li
|
||||
key={label}
|
||||
className="flex min-h-16 flex-col items-center justify-center gap-1.5 rounded-xl border border-slate-200 bg-slate-50 px-2 py-3 text-xs font-semibold text-slate-700 min-[420px]:min-h-14 min-[420px]:flex-row min-[420px]:gap-2"
|
||||
>
|
||||
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-lg border border-slate-200 bg-white text-slate-600">
|
||||
<Icon size={16} strokeWidth={1.8} aria-hidden="true" />
|
||||
</span>
|
||||
<span className="whitespace-nowrap">{label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user