|
|
@@ -1,224 +1,263 @@
|
|
|
-import { useQuery } from '@tanstack/react-query'
|
|
|
+/**
|
|
|
+ * Dashboard — 今日工作台(方案 A+B 改造版)。
|
|
|
+ *
|
|
|
+ * 原版是平台运维视角(统计卡 + 运行状态表),对 desktop 单用户无用。
|
|
|
+ * 现在的信息架构按使用频率排:
|
|
|
+ * 1. AI 助手入口(主角):自然语言输入 → /assistant/route 意图路由
|
|
|
+ * (方案 B,轻量 LLM 分类)→ 命中则直达该智能体对话并预填输入;
|
|
|
+ * 路由不可用/拿不准 → 回退手动选择器(方案 A 动线,永远可用)。
|
|
|
+ * 2. 继续上次的工作:跨智能体最近运行(运行中的排最前)。
|
|
|
+ * 3. 常用场景卡片:内置包典型任务,点击直达(无实例则先创建)。
|
|
|
+ * 4. 统计降级为页脚一行字。
|
|
|
+ */
|
|
|
+import { useState } from 'react'
|
|
|
+import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
-import { Bot, Play, CheckCircle2, AlertCircle, Zap, MessageSquare } from 'lucide-react'
|
|
|
+import {
|
|
|
+ ArrowRight, Bot, FileSearch, ClipboardList, Mail, NotebookPen,
|
|
|
+ Loader2, MessageSquare, Route, X,
|
|
|
+} from 'lucide-react'
|
|
|
+import toast from 'react-hot-toast'
|
|
|
import { statusApi } from '../api/status'
|
|
|
-import { agentsApi } from '../api/agents'
|
|
|
-import { Card, StatCard, PageHeader, Badge, HealthDot, Spinner, EmptyState } from '../components/ui'
|
|
|
-
|
|
|
-function relativeTime(iso: string | null) {
|
|
|
- if (!iso) return '—'
|
|
|
- const diff = Date.now() - new Date(iso).getTime()
|
|
|
- const m = Math.floor(diff / 60_000)
|
|
|
- if (m < 1) return '刚刚'
|
|
|
- if (m < 60) return `${m} 分钟前`
|
|
|
- const h = Math.floor(m / 60)
|
|
|
- if (h < 24) return `${h} 小时前`
|
|
|
- return `${Math.floor(h / 24)} 天前`
|
|
|
+import { agentsApi, type Agent } from '../api/agents'
|
|
|
+import { agentpacksApi } from '../api/agentpacks'
|
|
|
+import { Card, Spinner, Badge } from '../components/ui'
|
|
|
+
|
|
|
+// 常用场景 → 内置包映射(包未安装时卡片自动隐藏)
|
|
|
+const SCENARIOS = [
|
|
|
+ { packId: 'research.top-journal-reviewer', icon: FileSearch,
|
|
|
+ title: '审一篇论文', subtitle: '顶刊水准评审报告' },
|
|
|
+ { packId: 'teaching.exam-builder', icon: ClipboardList,
|
|
|
+ title: '出一份试卷', subtitle: '细目表 + A/B 卷' },
|
|
|
+ { packId: 'service.academic-letters', icon: Mail,
|
|
|
+ title: '写推荐信', subtitle: '中英文、事例驱动' },
|
|
|
+ { packId: 'teaching.course-designer', icon: NotebookPen,
|
|
|
+ title: '设计一门课', subtitle: '大纲 + 日历 + 教案' },
|
|
|
+]
|
|
|
+
|
|
|
+function greeting(): string {
|
|
|
+ const h = new Date().getHours()
|
|
|
+ if (h < 6) return '夜深了'
|
|
|
+ if (h < 12) return '早上好'
|
|
|
+ if (h < 18) return '下午好'
|
|
|
+ return '晚上好'
|
|
|
}
|
|
|
|
|
|
-function healthBadge(level: string) {
|
|
|
- const map: Record<string, 'green' | 'yellow' | 'red' | 'default'> = {
|
|
|
- healthy: 'green',
|
|
|
- degraded: 'yellow',
|
|
|
- warning: 'yellow',
|
|
|
- critical: 'red',
|
|
|
- no_data: 'default',
|
|
|
- }
|
|
|
- const labels: Record<string, string> = {
|
|
|
- healthy: '健康',
|
|
|
- degraded: '轻微异常',
|
|
|
- warning: '警告',
|
|
|
- critical: '严重',
|
|
|
- no_data: '无数据',
|
|
|
- }
|
|
|
- return <Badge variant={map[level] ?? 'default'}>{labels[level] ?? level}</Badge>
|
|
|
+function runStatusBadge(status: string) {
|
|
|
+ if (status === 'running') return <Badge variant="yellow">运行中</Badge>
|
|
|
+ if (status === 'completed') return <Badge variant="green">已完成</Badge>
|
|
|
+ if (status === 'cancelled') return <Badge variant="default">已取消</Badge>
|
|
|
+ return <Badge variant="red">失败</Badge>
|
|
|
}
|
|
|
|
|
|
export default function Dashboard() {
|
|
|
const navigate = useNavigate()
|
|
|
+ const qc = useQueryClient()
|
|
|
+ const [input, setInput] = useState('')
|
|
|
+ const [routing, setRouting] = useState(false)
|
|
|
+ const [pickerOpen, setPickerOpen] = useState(false)
|
|
|
+ const [creatingPack, setCreatingPack] = useState<string | null>(null)
|
|
|
|
|
|
- const { data: status, isLoading: loadingStatus, isError } = useQuery({
|
|
|
- queryKey: ['status'],
|
|
|
- queryFn: statusApi.platform,
|
|
|
- refetchInterval: 15_000,
|
|
|
- retry: false,
|
|
|
+ const { data: agentData } = useQuery({
|
|
|
+ queryKey: ['agents'], queryFn: agentsApi.list, retry: false,
|
|
|
})
|
|
|
-
|
|
|
- const { data: agentStatus, isLoading: loadingAgents } = useQuery({
|
|
|
- queryKey: ['status-agents'],
|
|
|
- queryFn: statusApi.agents,
|
|
|
- refetchInterval: 15_000,
|
|
|
- retry: false,
|
|
|
+ const { data: recent } = useQuery({
|
|
|
+ queryKey: ['recent-runs'], queryFn: () => statusApi.recentRuns(5),
|
|
|
+ retry: false, refetchInterval: 15000,
|
|
|
})
|
|
|
-
|
|
|
- const { data: agentList } = useQuery({
|
|
|
- queryKey: ['agents'],
|
|
|
- queryFn: agentsApi.list,
|
|
|
- retry: false,
|
|
|
+ const { data: platform } = useQuery({
|
|
|
+ queryKey: ['platform-status'], queryFn: statusApi.platform, retry: false,
|
|
|
+ })
|
|
|
+ const { data: packData } = useQuery({
|
|
|
+ queryKey: ['agentpacks'], queryFn: agentpacksApi.list, retry: false,
|
|
|
})
|
|
|
|
|
|
- const loading = loadingStatus || loadingAgents
|
|
|
+ const agents = agentData?.agents ?? []
|
|
|
+ const installedPacks = new Set((packData?.agentpacks ?? []).map(p => p.id))
|
|
|
+
|
|
|
+ const gotoChat = (agentId: string, q?: string) =>
|
|
|
+ navigate(`/chat/${agentId}${q ? `?q=${encodeURIComponent(q)}` : ''}`)
|
|
|
+
|
|
|
+ // ── AI 助手入口提交:先试意图路由(B),失败回退手动选择(A)──
|
|
|
+ async function submit() {
|
|
|
+ const q = input.trim()
|
|
|
+ if (!q) return
|
|
|
+ if (agents.length === 0) {
|
|
|
+ toast('先创建一个智能体(试试下方场景卡片)')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ setRouting(true)
|
|
|
+ try {
|
|
|
+ const r = await statusApi.assistantRoute(q)
|
|
|
+ if (r.matched && r.agent_id) {
|
|
|
+ toast.success(`交给「${r.agent_name}」${r.reason ? ` — ${r.reason}` : ''}`,
|
|
|
+ { duration: 3500 })
|
|
|
+ gotoChat(r.agent_id, q)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ } catch { /* 路由不可用 → 手动选择 */ }
|
|
|
+ finally { setRouting(false) }
|
|
|
+ setPickerOpen(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 场景卡片点击:已有该包实例 → 直达;没有 → 创建后直达 ──
|
|
|
+ async function openScenario(packId: string, title: string) {
|
|
|
+ const existing = agents
|
|
|
+ .filter(a => a.agent_template === packId)
|
|
|
+ .sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''))[0]
|
|
|
+ if (existing) { gotoChat(existing.id); return }
|
|
|
+ setCreatingPack(packId)
|
|
|
+ try {
|
|
|
+ const pack = (packData?.agentpacks ?? []).find(p => p.id === packId)
|
|
|
+ const res = await agentpacksApi.createAgent(packId, { name: pack?.name || title })
|
|
|
+ toast.success(`已创建「${res.name}」`)
|
|
|
+ qc.invalidateQueries({ queryKey: ['agents'] })
|
|
|
+ gotoChat(res.agent_id)
|
|
|
+ } catch (e) {
|
|
|
+ toast.error(e instanceof Error ? e.message : '创建失败')
|
|
|
+ } finally {
|
|
|
+ setCreatingPack(null)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const scenarios = SCENARIOS.filter(s => installedPacks.has(s.packId))
|
|
|
|
|
|
return (
|
|
|
- <div className="p-8 max-w-6xl mx-auto">
|
|
|
- <PageHeader
|
|
|
- title="仪表盘"
|
|
|
- description="平台运行概览"
|
|
|
- action={
|
|
|
- <button
|
|
|
- onClick={() => navigate('/agents/new')}
|
|
|
- className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition-colors"
|
|
|
- >
|
|
|
- <Bot size={16} />
|
|
|
- 新建智能体
|
|
|
- </button>
|
|
|
- }
|
|
|
- />
|
|
|
-
|
|
|
- {isError ? (
|
|
|
- <div className="rounded-xl border border-yellow-200 bg-yellow-50 p-6 text-sm text-yellow-800">
|
|
|
- <p className="font-medium mb-1">无法连接到后端服务</p>
|
|
|
- <p className="text-yellow-700">请先启动:
|
|
|
- <code className="ml-1 bg-yellow-100 px-1.5 py-0.5 rounded font-mono text-xs">agentpaas serve --dev</code>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- ) : loading ? (
|
|
|
- <div className="flex justify-center py-16"><Spinner size={32} /></div>
|
|
|
- ) : (
|
|
|
- <>
|
|
|
- {/* Stat cards */}
|
|
|
- <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
|
- <StatCard
|
|
|
- label="活跃智能体"
|
|
|
- value={status?.agents_active ?? 0}
|
|
|
- icon={<Bot size={22} />}
|
|
|
- />
|
|
|
- <StatCard
|
|
|
- label="总运行次数"
|
|
|
- value={status?.runs_total ?? 0}
|
|
|
- sub={`成功率 ${status?.success_rate ?? '—'}`}
|
|
|
- icon={<Play size={22} />}
|
|
|
- />
|
|
|
- <StatCard
|
|
|
- label="平均延迟"
|
|
|
- value={status?.avg_latency_ms ? `${status.avg_latency_ms} ms` : '—'}
|
|
|
- icon={<Zap size={22} />}
|
|
|
- />
|
|
|
- <StatCard
|
|
|
- label="累计 Token"
|
|
|
- value={
|
|
|
- status?.total_tokens
|
|
|
- ? status.total_tokens > 1_000_000
|
|
|
- ? `${(status.total_tokens / 1_000_000).toFixed(1)}M`
|
|
|
- : `${Math.round(status.total_tokens / 1000)}K`
|
|
|
- : '0'
|
|
|
- }
|
|
|
- icon={<MessageSquare size={22} />}
|
|
|
- />
|
|
|
+ <div className="p-8 max-w-3xl mx-auto">
|
|
|
+ {/* ── 1. AI 助手入口(主角)── */}
|
|
|
+ <div className="mb-8">
|
|
|
+ <h1 className="text-xl font-semibold text-gray-900 mb-1">{greeting()}</h1>
|
|
|
+ <p className="text-sm text-gray-500 mb-4">今天想做什么?描述你的任务,我来找合适的智能体。</p>
|
|
|
+ <div className="bg-white border border-gray-200 rounded-2xl shadow-sm focus-within:border-indigo-300 focus-within:ring-2 focus-within:ring-indigo-100 transition-all">
|
|
|
+ <textarea
|
|
|
+ value={input}
|
|
|
+ onChange={e => setInput(e.target.value)}
|
|
|
+ onKeyDown={e => {
|
|
|
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit() }
|
|
|
+ }}
|
|
|
+ placeholder="例如:帮我审这篇论文 ~/Papers/draft.pdf,目标期刊 TSE…"
|
|
|
+ rows={2}
|
|
|
+ className="w-full px-4 pt-3 pb-1 text-sm resize-none focus:outline-none rounded-2xl"
|
|
|
+ />
|
|
|
+ <div className="flex items-center justify-between px-4 pb-3">
|
|
|
+ <span className="text-[11px] text-gray-400 flex items-center gap-1">
|
|
|
+ <Route size={12} /> 自动分发给合适的智能体(也可手动选择)
|
|
|
+ </span>
|
|
|
+ <button
|
|
|
+ onClick={submit}
|
|
|
+ disabled={!input.trim() || routing}
|
|
|
+ className="flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-200 disabled:text-gray-400 text-white text-sm px-4 py-1.5 rounded-xl transition-colors"
|
|
|
+ >
|
|
|
+ {routing ? <Loader2 size={14} className="animate-spin" /> : <ArrowRight size={14} />}
|
|
|
+ {routing ? '分析中' : '开始'}
|
|
|
+ </button>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* Agent status table */}
|
|
|
- <Card padding={false} className="overflow-hidden">
|
|
|
- <div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
|
- <h2 className="text-sm font-semibold text-gray-900">智能体运行状态</h2>
|
|
|
- <span className="text-xs text-gray-400">每 15 秒自动刷新</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- {!agentStatus?.agents?.length ? (
|
|
|
- <EmptyState
|
|
|
- icon={<Bot size={40} />}
|
|
|
- title="还没有智能体"
|
|
|
- description="创建第一个智能体开始体验"
|
|
|
- action={
|
|
|
- <button
|
|
|
- onClick={() => navigate('/agents/new')}
|
|
|
- className="px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700"
|
|
|
- >
|
|
|
- 新建智能体
|
|
|
- </button>
|
|
|
- }
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <div className="overflow-x-auto">
|
|
|
- <table className="w-full text-sm">
|
|
|
- <thead className="bg-gray-50 border-b border-gray-100">
|
|
|
- <tr>
|
|
|
- {['名称', '状态', '运行次数', '成功率', '平均延迟', '最近运行', '操作'].map(h => (
|
|
|
- <th
|
|
|
- key={h}
|
|
|
- className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide"
|
|
|
- >
|
|
|
- {h}
|
|
|
- </th>
|
|
|
- ))}
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody className="divide-y divide-gray-50">
|
|
|
- {agentStatus.agents.map(a => (
|
|
|
- <tr key={a.agent_id} className="hover:bg-gray-50 transition-colors">
|
|
|
- <td className="px-6 py-4 font-medium text-gray-900">{a.name}</td>
|
|
|
- <td className="px-6 py-4">
|
|
|
- <div className="flex items-center gap-1.5">
|
|
|
- <HealthDot level={a.health.level} />
|
|
|
- {healthBadge(a.health.level)}
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- <td className="px-6 py-4 text-gray-600">{a.runs_total}</td>
|
|
|
- <td className="px-6 py-4 text-gray-600">{a.success_rate}</td>
|
|
|
- <td className="px-6 py-4 text-gray-600">
|
|
|
- {a.avg_latency_ms ? `${a.avg_latency_ms} ms` : '—'}
|
|
|
- </td>
|
|
|
- <td className="px-6 py-4 text-gray-500 text-xs">{relativeTime(a.last_run)}</td>
|
|
|
- <td className="px-6 py-4">
|
|
|
- <button
|
|
|
- onClick={() => navigate(`/chat/${a.agent_id}`)}
|
|
|
- className="text-indigo-600 hover:text-indigo-800 text-xs font-medium"
|
|
|
- >
|
|
|
- 对话
|
|
|
- </button>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- ))}
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ {/* ── 2. 继续上次的工作 ── */}
|
|
|
+ {(recent?.items?.length ?? 0) > 0 && (
|
|
|
+ <section className="mb-8">
|
|
|
+ <h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
|
+ 继续上次的工作
|
|
|
+ </h2>
|
|
|
+ <Card className="divide-y divide-gray-100 p-0 overflow-hidden">
|
|
|
+ {recent!.items.map(r => (
|
|
|
+ <button
|
|
|
+ key={r.run_id}
|
|
|
+ onClick={() => gotoChat(r.agent_id)}
|
|
|
+ className="w-full flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors text-left"
|
|
|
+ >
|
|
|
+ <MessageSquare size={15} className="text-gray-400 shrink-0" />
|
|
|
+ <span className="text-sm font-medium text-gray-900 shrink-0">{r.agent_name}</span>
|
|
|
+ <span className="text-xs text-gray-400 truncate flex-1">{r.input_preview}</span>
|
|
|
+ {runStatusBadge(r.status)}
|
|
|
+ <ArrowRight size={13} className="text-gray-300 shrink-0" />
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
</Card>
|
|
|
+ </section>
|
|
|
+ )}
|
|
|
|
|
|
- {/* Quick actions */}
|
|
|
- {!!agentList?.agents?.length && (
|
|
|
- <div className="mt-6 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
- <Card className="hover:shadow-md transition-shadow cursor-pointer" onClick={() => navigate('/agents')}>
|
|
|
- <div className="flex items-center gap-4">
|
|
|
- <div className="p-2 bg-indigo-50 rounded-lg">
|
|
|
- <Bot size={20} className="text-indigo-600" />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <p className="text-sm font-medium text-gray-900">管理智能体</p>
|
|
|
- <p className="text-xs text-gray-500">创建、编辑、删除智能体</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </Card>
|
|
|
+ {/* ── 3. 常用场景 ── */}
|
|
|
+ {scenarios.length > 0 && (
|
|
|
+ <section className="mb-8">
|
|
|
+ <h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
|
+ 常用场景
|
|
|
+ </h2>
|
|
|
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
|
|
+ {scenarios.map(s => {
|
|
|
+ const Icon = s.icon
|
|
|
+ const busy = creatingPack === s.packId
|
|
|
+ return (
|
|
|
+ <button
|
|
|
+ key={s.packId}
|
|
|
+ onClick={() => openScenario(s.packId, s.title)}
|
|
|
+ disabled={busy}
|
|
|
+ className="bg-white border border-gray-200 rounded-xl p-3.5 text-left hover:border-indigo-300 hover:shadow-sm transition-all disabled:opacity-60"
|
|
|
+ >
|
|
|
+ {busy
|
|
|
+ ? <Loader2 size={18} className="text-indigo-500 animate-spin" />
|
|
|
+ : <Icon size={18} className="text-indigo-500" />}
|
|
|
+ <p className="text-sm font-medium text-gray-900 mt-2">{s.title}</p>
|
|
|
+ <p className="text-xs text-gray-400 mt-0.5">{s.subtitle}</p>
|
|
|
+ </button>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ )}
|
|
|
|
|
|
- <Card
|
|
|
- className="hover:shadow-md transition-shadow cursor-pointer"
|
|
|
- onClick={() => navigate(`/chat/${agentList.agents[0].id}`)}
|
|
|
- >
|
|
|
- <div className="flex items-center gap-4">
|
|
|
- <div className="p-2 bg-green-50 rounded-lg">
|
|
|
- <MessageSquare size={20} className="text-green-600" />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <p className="text-sm font-medium text-gray-900">开始对话</p>
|
|
|
- <p className="text-xs text-gray-500">
|
|
|
- 与「{agentList.agents[0].name}」对话
|
|
|
- </p>
|
|
|
+ {/* 全空态:没有任何智能体也没有场景包 */}
|
|
|
+ {agents.length === 0 && scenarios.length === 0 && (
|
|
|
+ <Card className="text-center py-10 mb-8">
|
|
|
+ <Bot size={36} className="mx-auto text-gray-300 mb-3" />
|
|
|
+ <p className="text-sm text-gray-500">还没有智能体 —— 到「智能体包」页用内置包一键创建</p>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* ── 4. 统计页脚 ── */}
|
|
|
+ {platform && (
|
|
|
+ <p className="text-xs text-gray-400 text-right">
|
|
|
+ 累计 {platform.runs_total} 次运行 · {(platform.total_tokens / 10000).toFixed(1)} 万 token
|
|
|
+ <button onClick={() => navigate('/providers')} className="ml-2 text-indigo-400 hover:text-indigo-600">
|
|
|
+ 模型与用量 →
|
|
|
+ </button>
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* ── 手动选择器(路由不可用/拿不准时的回退)── */}
|
|
|
+ {pickerOpen && (
|
|
|
+ <div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50" onClick={() => setPickerOpen(false)}>
|
|
|
+ <div className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-5" onClick={e => e.stopPropagation()}>
|
|
|
+ <div className="flex items-center justify-between mb-3">
|
|
|
+ <h3 className="text-sm font-semibold text-gray-900">交给哪个智能体?</h3>
|
|
|
+ <button onClick={() => setPickerOpen(false)} className="text-gray-300 hover:text-gray-500">
|
|
|
+ <X size={16} />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div className="max-h-80 overflow-y-auto divide-y divide-gray-100">
|
|
|
+ {agents.map((a: Agent) => (
|
|
|
+ <button
|
|
|
+ key={a.id}
|
|
|
+ onClick={() => { setPickerOpen(false); gotoChat(a.id, input.trim()) }}
|
|
|
+ className="w-full flex items-center gap-3 px-2 py-2.5 hover:bg-gray-50 rounded-lg text-left"
|
|
|
+ >
|
|
|
+ <Bot size={15} className="text-indigo-400 shrink-0" />
|
|
|
+ <div className="min-w-0">
|
|
|
+ <p className="text-sm font-medium text-gray-900 truncate">{a.name}</p>
|
|
|
+ {a.description && (
|
|
|
+ <p className="text-xs text-gray-400 truncate">{a.description}</p>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </Card>
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+ {agents.length === 0 && (
|
|
|
+ <div className="py-6 text-center"><Spinner /></div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- )}
|
|
|
- </>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
)}
|
|
|
</div>
|
|
|
)
|