Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a206a43063 | ||
|
|
998bd0ce06 | ||
|
|
6b6465b5e3 | ||
|
|
57da50b8d6 | ||
|
|
f87f6031de | ||
|
|
5b686824c5 | ||
|
|
95487d362f | ||
|
|
f496546cc0 | ||
|
|
9423d7e892 | ||
|
|
062f984262 |
No files matched your search
@@ -2,6 +2,8 @@
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
CopyIcon,
|
||||
GlobeIcon,
|
||||
InfoIcon,
|
||||
@@ -85,6 +87,10 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
agent.skills ?? [],
|
||||
);
|
||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||
const [skillsExpanded, setSkillsExpanded] = useState(false);
|
||||
|
||||
const agentSkills = agent.skills ?? [];
|
||||
const showSkillsToggle = agentSkills.length >= 3;
|
||||
|
||||
// The /api/agents list response intentionally omits `soul` (perf optimization
|
||||
// — SOUL.md is only read on demand). Fetch the detail row when the edit
|
||||
@@ -111,6 +117,10 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
const canFavorite = !isOwned;
|
||||
const isFavorite = !!agent.is_favorite;
|
||||
|
||||
useEffect(() => {
|
||||
setSkillsExpanded(false);
|
||||
}, [agent.id, agentSkills.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editOpen) {
|
||||
return;
|
||||
@@ -382,11 +392,13 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{agent.description && (
|
||||
<CardDescription className="mt-2 line-clamp-2 text-sm">
|
||||
{desensitize(agent.description)}
|
||||
</CardDescription>
|
||||
)}
|
||||
<div className="mt-2 min-h-10">
|
||||
{agent.description ? (
|
||||
<CardDescription className="line-clamp-2 text-sm">
|
||||
{desensitize(agent.description)}
|
||||
</CardDescription>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{(agent.tags?.length ?? 0) > 0 && (
|
||||
@@ -396,36 +408,73 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
)}
|
||||
|
||||
{(agent.tool_groups?.length ?? agent.skills?.length ?? 0) > 0 && (
|
||||
<CardContent className="pt-0 pb-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.tool_groups?.map((group) => (
|
||||
<Badge
|
||||
key={`tg:${group}`}
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
<CardContent className="min-w-0 pt-0 pb-3">
|
||||
{(agent.tool_groups?.length ?? 0) > 0 && (
|
||||
<div className="mb-1 flex flex-wrap gap-1">
|
||||
{agent.tool_groups?.map((group) => (
|
||||
<Badge
|
||||
key={`tg:${group}`}
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
{desensitize(group)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{agentSkills.length > 0 && (
|
||||
<div className="relative min-w-0">
|
||||
{showSkillsToggle && (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground absolute top-0 right-0 z-10 h-6 w-6 shrink-0"
|
||||
title={skillsExpanded ? "收起技能" : "展开全部技能"}
|
||||
aria-expanded={skillsExpanded}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSkillsExpanded((open) => !open);
|
||||
}}
|
||||
>
|
||||
{skillsExpanded ? (
|
||||
<ChevronUpIcon className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 flex-wrap gap-1",
|
||||
showSkillsToggle && "pr-7",
|
||||
!skillsExpanded &&
|
||||
showSkillsToggle &&
|
||||
"max-h-12 overflow-hidden",
|
||||
)}
|
||||
>
|
||||
{desensitize(group)}
|
||||
</Badge>
|
||||
))}
|
||||
{agent.skills?.map((skill) => (
|
||||
<Badge
|
||||
key={`sk:${skill}`}
|
||||
variant="secondary"
|
||||
className="hover:bg-secondary/70 cursor-pointer text-xs"
|
||||
title={t.settings.skills.viewDetailAction}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDetailSkillName(skill);
|
||||
}}
|
||||
>
|
||||
{desensitize(skill)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{agentSkills.map((skill) => (
|
||||
<Badge
|
||||
key={`sk:${skill}`}
|
||||
variant="secondary"
|
||||
className="hover:bg-secondary/70 max-w-full min-w-0 cursor-pointer text-xs"
|
||||
title={t.settings.skills.viewDetailAction}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDetailSkillName(skill);
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{desensitize(skill)}</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
<CardFooter className="mt-auto flex flex-col items-stretch gap-2 pt-3">
|
||||
|
||||
{(canEdit || canDelete || isOwned || isAdmin || canFavorite) && (
|
||||
<div className="flex flex-wrap items-center justify-end gap-1">
|
||||
{canFavorite && (
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
||||
import { useAgent } from "@/core/agents";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
/** 与 `AgentCard` 编辑弹框同布局,仅查看、不可修改。 */
|
||||
export function AgentInspectDialog({
|
||||
agentId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
agentId: string | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { agent, isLoading } = useAgent(open ? agentId : null);
|
||||
const { skills, isLoading: isSkillsLoading } = useSkills();
|
||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||
|
||||
const enabledSkills = useMemo(
|
||||
() => skills.filter((s) => s.enabled),
|
||||
[skills],
|
||||
);
|
||||
|
||||
const selectedSkills = useMemo(
|
||||
() =>
|
||||
(agent?.skills ?? []).map((name) => {
|
||||
const meta = enabledSkills.find((s) => s.name === name);
|
||||
return { name, description: meta?.description };
|
||||
}),
|
||||
[agent?.skills, enabledSkills],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
查看智能体
|
||||
{agent ? `:${desensitize(agent.name)}` : ""}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
仅查看名称、描述、技能白名单、SOUL 与发布状态,不可修改。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isLoading && !agent ? (
|
||||
<p className="text-muted-foreground py-8 text-center text-sm">
|
||||
{t.common.loading}
|
||||
</p>
|
||||
) : !agent ? (
|
||||
<p className="text-muted-foreground py-8 text-center text-sm">
|
||||
无法加载智能体信息
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">名称</label>
|
||||
<Input
|
||||
readOnly
|
||||
value={desensitize(agent.name)}
|
||||
className="bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
<Textarea
|
||||
readOnly
|
||||
value={desensitize(agent.description ?? "")}
|
||||
placeholder="暂无描述"
|
||||
className="min-h-20 bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">技能白名单</label>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
已选 {selectedSkills.length} 个
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-h-[240px] overflow-y-auto rounded-md border bg-muted/20 p-2">
|
||||
{isSkillsLoading ? (
|
||||
<p className="text-muted-foreground text-sm">技能加载中...</p>
|
||||
) : selectedSkills.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">未配置技能</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{selectedSkills.map((skill) => (
|
||||
<div
|
||||
key={skill.name}
|
||||
className="flex w-full min-w-0 items-center"
|
||||
>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-8 min-w-0 flex-1 justify-start rounded-r-none px-2 font-normal"
|
||||
title={desensitize(skill.description ?? "")}
|
||||
>
|
||||
<span className="truncate">
|
||||
{desensitize(skill.name)}
|
||||
</span>
|
||||
</Badge>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
onClick={() => setDetailSkillName(skill.name)}
|
||||
title={t.settings.skills.viewDetailAction}
|
||||
className="h-8 w-7 shrink-0 rounded-l-none border-l-0"
|
||||
>
|
||||
<InfoIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedSkills.length === 0 && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
不选任何技能会保存为无技能(skills = [])。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">SOUL</label>
|
||||
<Textarea
|
||||
readOnly
|
||||
value={agent.soul ?? ""}
|
||||
placeholder="暂无 SOUL 内容"
|
||||
className="min-h-40 bg-muted/30 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border bg-muted/20 p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">公开发布</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{agent.is_private
|
||||
? "已加入私密广场,不可发布。"
|
||||
: "打开后,该智能体对所有用户可见。"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm tabular-nums">
|
||||
{agent.published && !agent.is_private ? "是" : "否"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border bg-muted/20 p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">加入私密广场</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
打开后,该智能体仅在私密广场显示,需输入密码才能查看;与「公开发布」互斥。
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm tabular-nums">
|
||||
{agent.is_private ? "是" : "否"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<SkillDetailDialog
|
||||
skillName={detailSkillName}
|
||||
open={detailSkillName !== null}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) setDetailSkillName(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { PenLineIcon, PlusIcon, SearchIcon, Trash2Icon } from "lucide-react";
|
||||
import { PenLineIcon, PlusIcon, SearchIcon, Trash2Icon, Heart, BookOpen, Map, Zap, Target, ShieldAlert } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -464,13 +464,41 @@ const HISTORY_LABELS: Record<string, string> = {
|
||||
longTermBackground: "长期背景",
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
preference: "偏好",
|
||||
knowledge: "知识",
|
||||
context: "背景",
|
||||
behavior: "行为",
|
||||
goal: "目标",
|
||||
correction: "纠偏",
|
||||
},
|
||||
en: {
|
||||
preference: "Preference",
|
||||
knowledge: "Knowledge",
|
||||
context: "Context",
|
||||
behavior: "Behavior",
|
||||
goal: "Goal",
|
||||
correction: "Correction",
|
||||
},
|
||||
};
|
||||
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
preference: "bg-blue-100 text-blue-700",
|
||||
knowledge: "bg-green-100 text-green-700",
|
||||
context: "bg-yellow-100 text-yellow-700",
|
||||
behavior: "bg-purple-100 text-purple-700",
|
||||
goal: "bg-orange-100 text-orange-700",
|
||||
correction: "bg-red-100 text-red-700",
|
||||
preference: "bg-blue-50 text-blue-700 border border-blue-200/50 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-900/30",
|
||||
knowledge: "bg-green-50 text-green-700 border border-green-200/50 dark:bg-green-950/30 dark:text-green-400 dark:border-green-900/30",
|
||||
context: "bg-amber-50 text-amber-700 border border-amber-200/50 dark:bg-amber-950/30 dark:text-amber-400 dark:border-amber-900/30",
|
||||
behavior: "bg-purple-50 text-purple-700 border border-purple-200/50 dark:bg-purple-950/30 dark:text-purple-400 dark:border-purple-900/30",
|
||||
goal: "bg-orange-50 text-orange-700 border border-orange-200/50 dark:bg-orange-950/30 dark:text-orange-400 dark:border-orange-900/30",
|
||||
correction: "bg-red-50 text-red-700 border border-red-200/50 dark:bg-red-950/30 dark:text-red-400 dark:border-red-900/30",
|
||||
};
|
||||
|
||||
const CATEGORY_ICONS: Record<string, React.ElementType> = {
|
||||
preference: Heart,
|
||||
knowledge: BookOpen,
|
||||
context: Map,
|
||||
behavior: Zap,
|
||||
goal: Target,
|
||||
correction: ShieldAlert,
|
||||
};
|
||||
|
||||
/** V1 记忆只读视图:展示 memory.json 的全部字段,Tab 布局。 */
|
||||
@@ -484,6 +512,9 @@ function MemoryV1View({
|
||||
memory_usage: string;
|
||||
};
|
||||
}) {
|
||||
const { locale } = useI18n();
|
||||
const isZh = locale.startsWith("zh");
|
||||
const labelMap = isZh ? CATEGORY_LABELS.zh : CATEGORY_LABELS.en;
|
||||
const ctxEntries = Object.entries(memory.user_context ?? {}).filter(([, v]) => v);
|
||||
const historyEntries = Object.entries(memory.history ?? {}).filter(([, v]) => v);
|
||||
|
||||
@@ -519,37 +550,63 @@ function MemoryV1View({
|
||||
暂无事实记录,对话后系统会自动提取。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{memory.facts.map((fact) => (
|
||||
<div key={fact.id} className="rounded-md border p-3 space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-xs font-medium ${
|
||||
CATEGORY_COLORS[fact.category] ?? "bg-gray-100 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{fact.category}
|
||||
</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary"
|
||||
style={{ width: `${Math.round(fact.confidence * 100)}%` }}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 pt-2">
|
||||
{memory.facts.map((fact) => {
|
||||
const categoryLabel = labelMap[fact.category] ?? fact.category;
|
||||
const Icon = CATEGORY_ICONS[fact.category] ?? PlusIcon;
|
||||
return (
|
||||
<div
|
||||
key={fact.id}
|
||||
className="group relative flex flex-col justify-between rounded-xl border border-border/40 bg-gradient-to-br from-card to-card/50 hover:from-card hover:to-card hover:border-primary/30 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 p-5 space-y-4 dark:border-[#213D5E]/40 dark:from-[#022248]/40 dark:to-[#022248]/10 dark:hover:from-[#022248]/60 dark:hover:to-[#022248]/30 overflow-hidden"
|
||||
>
|
||||
{/* Subtle top highlight */}
|
||||
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium border shadow-sm ${
|
||||
CATEGORY_COLORS[fact.category] ?? "bg-gray-100 text-gray-700 border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
{categoryLabel}
|
||||
</span>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className="text-[11px] text-muted-foreground font-semibold tracking-wider">
|
||||
{Math.round(fact.confidence * 100)}%
|
||||
</span>
|
||||
{/* Confidence Progress Bar */}
|
||||
<div className="h-1 w-12 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all duration-500"
|
||||
style={{ width: `${Math.round(fact.confidence * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[14px] text-foreground/90 leading-relaxed whitespace-pre-wrap [overflow-wrap:anywhere]">
|
||||
{desensitize(fact.content)}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{Math.round(fact.confidence * 100)}%
|
||||
</span>
|
||||
|
||||
{fact.sourceError && (
|
||||
<div className="mt-auto pt-2">
|
||||
<div className="rounded-lg bg-red-50/80 dark:bg-red-950/30 border border-red-100 dark:border-red-900/50 p-3 text-xs text-red-600 dark:text-red-400 shadow-sm">
|
||||
<div className="flex items-center gap-1.5 font-semibold mb-1">
|
||||
<ShieldAlert className="w-3.5 h-3.5" />
|
||||
<span>避免</span>
|
||||
</div>
|
||||
<p className="leading-relaxed opacity-90">{desensitize(fact.sourceError)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(fact.content)}</p>
|
||||
{fact.sourceError && (
|
||||
<p className="rounded bg-red-50 px-2 py-1 text-xs text-red-600 dark:bg-[#311629] dark:text-[#EE212C]">
|
||||
避免:{desensitize(fact.sourceError)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -44,6 +51,7 @@ import {
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/components/ui/empty";
|
||||
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -322,6 +330,8 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
{!isLoading && !error && filteredSkills.length === 0 && (
|
||||
<EmptySkill onCreateSkill={handleCreateSkill} />
|
||||
)}
|
||||
|
||||
{/* —— 原纵向列表布局(保留备查)——
|
||||
{!isLoading && !error &&
|
||||
filteredSkills.length > 0 &&
|
||||
pagedSkills.map((skill) => (
|
||||
@@ -331,7 +341,21 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
))}
|
||||
))} */}
|
||||
{/* card */}
|
||||
{!isLoading && !error && filteredSkills.length > 0 && (
|
||||
<div className="grid grid-cols-1 items-stretch gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{pagedSkills.map((skill) => (
|
||||
<SkillRow
|
||||
key={skill.name}
|
||||
skill={skill}
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
className="h-full"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && totalPages > 1 && (
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
<Button
|
||||
@@ -384,10 +408,12 @@ export function SkillRow({
|
||||
skill,
|
||||
currentUserId,
|
||||
isAdmin,
|
||||
className,
|
||||
}: {
|
||||
skill: Skill;
|
||||
currentUserId: string | null;
|
||||
isAdmin: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const setPublished = useSetSkillPublished();
|
||||
@@ -473,6 +499,8 @@ export function SkillRow({
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{/* —— 原 Item 列表行布局(保留备查)——
|
||||
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
|
||||
<ItemContent
|
||||
className="cursor-pointer"
|
||||
@@ -640,7 +668,205 @@ export function SkillRow({
|
||||
</Button>
|
||||
)}
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</Item> */}
|
||||
|
||||
<Card
|
||||
className={cn(
|
||||
"group flex h-full min-w-0 cursor-pointer flex-col transition-shadow hover:shadow-md dark:!border-[#213D5E] dark:bg-[#02244C] dark:text-white",
|
||||
className,
|
||||
)}
|
||||
onClick={() => setDetailOpen(true)}
|
||||
>
|
||||
<CardHeader className="min-w-0 flex flex-col gap-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
||||
<SparklesIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2 overflow-hidden">
|
||||
<CardTitle
|
||||
className="min-w-0 flex-1 overflow-hidden text-base text-ellipsis whitespace-nowrap"
|
||||
title={desensitize(skill.name)}
|
||||
>
|
||||
{desensitize(skill.name)}
|
||||
</CardTitle>
|
||||
<Badge variant="secondary" className="shrink-0 text-xs">
|
||||
{isBuiltin
|
||||
? t.settings.skills.badgeBuiltin
|
||||
: skill.category}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-1.5 flex min-h-5 flex-wrap items-center gap-1">
|
||||
{isOwned && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300 text-xs"
|
||||
>
|
||||
{t.settings.skills.badgeMine}
|
||||
</Badge>
|
||||
)}
|
||||
{isFromOthers && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-blue-500/30 text-blue-700 dark:text-blue-300 text-xs"
|
||||
title={
|
||||
skill.owner_user_id
|
||||
? t.settings.skills.badgeFromOthersTitle.replace(
|
||||
"{user_id}",
|
||||
skill.owner_user_id,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{t.settings.skills.badgeFromOthers}
|
||||
</Badge>
|
||||
)}
|
||||
{isOwned && skill.published && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
|
||||
title={t.settings.skills.badgePublishedTitle}
|
||||
>
|
||||
<GlobeIcon className="size-3" />
|
||||
{t.settings.skills.badgePublished}
|
||||
</Badge>
|
||||
)}
|
||||
{isCustom && !isBuiltin && skill.source === "agent" && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-cyan-500/30 text-cyan-700 dark:text-cyan-300 text-xs"
|
||||
title={t.settings.skills.badgeSourceAgentTitle}
|
||||
>
|
||||
{t.settings.skills.badgeSourceAgent}
|
||||
</Badge>
|
||||
)}
|
||||
{isCustom && !isBuiltin && skill.source === "upload" && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-zinc-400/40 text-zinc-600 dark:text-zinc-400 text-xs"
|
||||
title={t.settings.skills.badgeSourceUploadTitle}
|
||||
>
|
||||
{t.settings.skills.badgeSourceUpload}
|
||||
</Badge>
|
||||
)}
|
||||
{skill.pinned && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-purple-500/30 text-purple-700 dark:text-purple-300 text-xs"
|
||||
title={t.settings.skills.badgePinnedTitle}
|
||||
>
|
||||
{t.settings.skills.badgePinned}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1.5 min-h-10 mb-3">
|
||||
{skill.description ? (
|
||||
<CardDescription className="line-clamp-2 text-sm">
|
||||
{desensitize(skill.description)}
|
||||
</CardDescription>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-0.5 min-h-4 text-xs leading-tight">
|
||||
{skill.use_count !== undefined && skill.use_count > 0
|
||||
? t.settings.skills.useCountInline.replace(
|
||||
"{count}",
|
||||
String(skill.use_count),
|
||||
)
|
||||
: t.settings.skills.useCountInlineZero}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 max-w-full overflow-hidden pb-0",
|
||||
(skill.tags?.length ?? 0) > 0 ? "pt-2.5" : "min-h-4",
|
||||
)}
|
||||
>
|
||||
{(skill.tags?.length ?? 0) > 0 && (
|
||||
<TagChipList tags={skill.tags!} className="w-full" />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardFooter
|
||||
className="cursor-default py-0 pt-1.5"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex w-full flex-wrap items-center justify-end gap-1">
|
||||
{isAdmin && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => setTagPickerOpen(true)}
|
||||
title={t.adminTags.manageTags}
|
||||
>
|
||||
<TagsIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{canManage && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
disabled={setPinned.isPending}
|
||||
onClick={() => void handleTogglePinned()}
|
||||
title={
|
||||
skill.pinned
|
||||
? t.settings.skills.unpinAction
|
||||
: t.settings.skills.pinAction
|
||||
}
|
||||
>
|
||||
{skill.pinned ? (
|
||||
<PinOffIcon className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<PinIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{canTogglePublish && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
disabled={setPublished.isPending}
|
||||
onClick={() => void handleTogglePublished(!skill.published)}
|
||||
title={
|
||||
skill.published
|
||||
? t.settings.skills.unpublishAction
|
||||
: t.settings.skills.publishAction
|
||||
}
|
||||
>
|
||||
{skill.published ? (
|
||||
<LockIcon className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<GlobeIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
disabled={duplicateSkillMutation.isPending}
|
||||
onClick={handleOpenDuplicate}
|
||||
title="复制为新技能"
|
||||
>
|
||||
<CopyIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{canManage && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
|
||||
onClick={() => setConfirmDeleteOpen(true)}
|
||||
title={t.settings.skills.deleteAction}
|
||||
>
|
||||
<Trash2Icon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
||||
@@ -17,20 +17,22 @@ export function TagChip({
|
||||
className?: string;
|
||||
}) {
|
||||
const color = resolveTagColor(tag.color);
|
||||
const label = desensitize(tag.name);
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium",
|
||||
"inline-flex max-w-full min-w-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium",
|
||||
color.chip,
|
||||
className,
|
||||
)}
|
||||
title={label}
|
||||
>
|
||||
{desensitize(tag.name)}
|
||||
<span className="min-w-0 truncate">{label}</span>
|
||||
{onRemove && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="hover:opacity-70"
|
||||
className="shrink-0 hover:opacity-70"
|
||||
aria-label={`remove ${tag.name}`}
|
||||
>
|
||||
<XIcon className="size-3" />
|
||||
@@ -50,7 +52,7 @@ export function TagChipList({
|
||||
}) {
|
||||
if (!tags || tags.length === 0) return null;
|
||||
return (
|
||||
<div className={cn("flex flex-wrap gap-1", className)}>
|
||||
<div className={cn("flex min-w-0 max-w-full flex-wrap gap-1", className)}>
|
||||
{tags.map((tag) => (
|
||||
<TagChip key={tag.name} tag={tag} />
|
||||
))}
|
||||
|
||||
@@ -621,10 +621,15 @@ export function useThreadStream({
|
||||
lockThreadRagMode(threadId);
|
||||
}
|
||||
|
||||
await thread.submit(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
const mergedContext = {
|
||||
...context,
|
||||
...extraContext,
|
||||
};
|
||||
|
||||
await thread.submit(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
type: "human",
|
||||
content: [
|
||||
{
|
||||
@@ -642,32 +647,33 @@ export function useThreadStream({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
threadId: threadId,
|
||||
streamSubgraphs: true,
|
||||
streamResumable: true,
|
||||
config: {
|
||||
recursion_limit: 1000,
|
||||
},
|
||||
context: {
|
||||
...extraContext,
|
||||
...context,
|
||||
thinking_enabled: context.mode !== "flash",
|
||||
is_plan_mode: context.mode === "pro" || context.mode === "ultra",
|
||||
subagent_enabled: context.mode === "ultra",
|
||||
reasoning_effort:
|
||||
context.reasoning_effort ??
|
||||
(context.mode === "ultra"
|
||||
? "high"
|
||||
: context.mode === "pro"
|
||||
? "medium"
|
||||
: context.mode === "thinking"
|
||||
? "low"
|
||||
: undefined),
|
||||
memory_injection_enabled: context.memory_injection_enabled ?? true,
|
||||
rag_mode_enabled: ragModeEnabled,
|
||||
thread_id: threadId,
|
||||
},
|
||||
{
|
||||
threadId: threadId,
|
||||
streamSubgraphs: true,
|
||||
streamResumable: true,
|
||||
config: {
|
||||
recursion_limit: 1000,
|
||||
},
|
||||
context: {
|
||||
...mergedContext,
|
||||
thinking_enabled: mergedContext.mode !== "flash",
|
||||
is_plan_mode:
|
||||
mergedContext.mode === "pro" || mergedContext.mode === "ultra",
|
||||
subagent_enabled: mergedContext.mode === "ultra",
|
||||
reasoning_effort:
|
||||
mergedContext.reasoning_effort ??
|
||||
(mergedContext.mode === "ultra"
|
||||
? "high"
|
||||
: mergedContext.mode === "pro"
|
||||
? "medium"
|
||||
: mergedContext.mode === "thinking"
|
||||
? "low"
|
||||
: undefined),
|
||||
memory_injection_enabled:
|
||||
mergedContext.memory_injection_enabled ?? true,
|
||||
rag_mode_enabled: ragModeEnabled,
|
||||
thread_id: threadId,
|
||||
},
|
||||
},
|
||||
);
|
||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
|
||||
@@ -102,17 +102,99 @@ import {
|
||||
// Panel IDs used as keys in the v4 Layout map
|
||||
const WRITING_ID = 'aw-writing';
|
||||
const DRAFT_ID = 'aw-draft';
|
||||
// 初始问答阶段(idle)专用的两栏 ID:左=问答助手,右=写作配置表单
|
||||
const SETUP_CHAT_ID = 'aw-setup-chat';
|
||||
const SETUP_FORM_ID = 'aw-setup-form';
|
||||
|
||||
// Match the original canvas page proportions (chat=35, artifact=65)
|
||||
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
|
||||
// idle 阶段镜像原比例:问答框在左占大头(65),配置表单在右占小头(35)
|
||||
const SETUP_LAYOUT = { [SETUP_CHAT_ID]: 65, [SETUP_FORM_ID]: 35 };
|
||||
|
||||
/**
|
||||
* 右侧面板:未开始写作(idle)时显示「写作助手 Q&A」,帮用户聊清并回填左侧表单;
|
||||
* 一旦开始写作(status !== idle)就切回草稿预览。
|
||||
* 写作配置表单面板(写作助手回填的目标),抽出复用:idle 时在右栏,开始写作后在左栏。
|
||||
*/
|
||||
function AIWritingRightPanel() {
|
||||
function WritingFormPanel({ hideAdvancedFields }: { hideAdvancedFields: boolean }) {
|
||||
return (
|
||||
// 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<AIWritingPanel hideAdvancedFields={hideAdvancedFields} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据写作状态切换两套左右布局:
|
||||
* - idle(初始问答阶段):左=「写作助手 Q&A」,右=「写作配置选项」表单;
|
||||
* - 开始写作后:回到原布局——左=写作配置/时间线面板,右=草稿预览。
|
||||
*/
|
||||
function AIWritingLayout({ hideAdvancedFields }: { hideAdvancedFields: boolean }) {
|
||||
const layoutRef = useRef<GroupImperativeHandle>(null);
|
||||
const { state } = useAIWriting();
|
||||
return state.status === 'idle' ? <WritingSetupChat /> : <AIWritingDraftPanel />;
|
||||
|
||||
if (state.status === 'idle') {
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
orientation="horizontal"
|
||||
groupRef={layoutRef}
|
||||
defaultLayout={SETUP_LAYOUT}
|
||||
className="h-full"
|
||||
>
|
||||
{/* 左侧:写作助手 Q&A */}
|
||||
<ResizablePanel
|
||||
id={SETUP_CHAT_ID}
|
||||
defaultSize={65}
|
||||
minSize={40}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<WritingSetupChat />
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
{/* 右侧:写作配置选项表单 */}
|
||||
<ResizablePanel
|
||||
id={SETUP_FORM_ID}
|
||||
defaultSize={35}
|
||||
minSize={20}
|
||||
className="overflow-hidden flex flex-col"
|
||||
>
|
||||
<WritingFormPanel hideAdvancedFields={hideAdvancedFields} />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
orientation="horizontal"
|
||||
groupRef={layoutRef}
|
||||
defaultLayout={DEFAULT_LAYOUT}
|
||||
className="h-full"
|
||||
>
|
||||
{/* 左侧:AI 写作面板,与原画布页左侧等宽 */}
|
||||
<ResizablePanel
|
||||
id={WRITING_ID}
|
||||
defaultSize={35}
|
||||
minSize={20}
|
||||
className="overflow-hidden flex flex-col"
|
||||
>
|
||||
<WritingFormPanel hideAdvancedFields={hideAdvancedFields} />
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
{/* 右侧:草稿预览面板 */}
|
||||
<ResizablePanel
|
||||
id={DRAFT_ID}
|
||||
defaultSize={65}
|
||||
minSize={40}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<AIWritingDraftPanel />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AIWritingPage({
|
||||
@@ -120,8 +202,6 @@ export default function AIWritingPage({
|
||||
}: {
|
||||
hideAdvancedFields?: boolean;
|
||||
} = {}) {
|
||||
const layoutRef = useRef<GroupImperativeHandle>(null);
|
||||
|
||||
return (
|
||||
<CanvasThreadProvider>
|
||||
<CanvasGraphProvider>
|
||||
@@ -131,37 +211,7 @@ export default function AIWritingPage({
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div className="h-full flex flex-col overflow-hidden bg-background">
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<ResizablePanelGroup
|
||||
orientation="horizontal"
|
||||
groupRef={layoutRef}
|
||||
defaultLayout={DEFAULT_LAYOUT}
|
||||
className="h-full"
|
||||
>
|
||||
{/* 左侧:AI 写作面板,与原画布页左侧等宽 */}
|
||||
<ResizablePanel
|
||||
id={WRITING_ID}
|
||||
defaultSize={35}
|
||||
minSize={20}
|
||||
className="overflow-hidden flex flex-col"
|
||||
>
|
||||
{/* 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析 */}
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<AIWritingPanel hideAdvancedFields={hideAdvancedFields} />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
{/* 右侧:草稿预览面板 */}
|
||||
<ResizablePanel
|
||||
id={DRAFT_ID}
|
||||
defaultSize={65}
|
||||
minSize={40}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<AIWritingRightPanel />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -220,8 +220,10 @@ export default function IframeChatPage() {
|
||||
prompt: data.payload?.prompt,
|
||||
pageContext: data.payload?.pageContext,
|
||||
screenshot: data.payload?.screenshot,
|
||||
model: data.payload?.model,
|
||||
requestId: data.requestId,
|
||||
filesCount: files.length,
|
||||
iframeContext,
|
||||
});
|
||||
|
||||
if (!submitText && files.length === 0) return;
|
||||
|
||||
@@ -79,3 +79,24 @@ test("keeps screenshot-only iframe submissions in the conversation flow", () =>
|
||||
assert.equal(result.files[0]?.filename, "shot.jpg");
|
||||
assert.equal(result.files[0]?.mediaType, "image/jpeg");
|
||||
});
|
||||
|
||||
test("maps iframe model selection into stream context model_name", () => {
|
||||
const result = parseBrainyChatSubmitMessage({
|
||||
source: "brainy-extension",
|
||||
type: "brainy:chat-submit",
|
||||
requestId: "iframe-456",
|
||||
payload: {
|
||||
text: "帮我总结这个页面",
|
||||
model: {
|
||||
label: "Qwen3-32B: 效果与速度均衡(推荐)",
|
||||
value: "OpenAI3",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.submitText, "帮我总结这个页面");
|
||||
assert.deepEqual(result.iframeContext, {
|
||||
brainy_request_id: "iframe-456",
|
||||
model_name: "OpenAI3",
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,11 @@ type BrainyScreenshot = {
|
||||
images?: BrainyScreenshotImage[];
|
||||
};
|
||||
|
||||
type BrainyModelSelection = {
|
||||
label?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export type BrainyChatSubmitMessage = {
|
||||
source?: string;
|
||||
type?: string;
|
||||
@@ -29,6 +34,7 @@ export type BrainyChatSubmitMessage = {
|
||||
prompt?: string;
|
||||
pageContext?: BrainyPageContext;
|
||||
screenshot?: BrainyScreenshot;
|
||||
model?: BrainyModelSelection;
|
||||
};
|
||||
requestId?: string;
|
||||
};
|
||||
@@ -261,13 +267,20 @@ export function parseBrainyChatSubmitMessage(
|
||||
hasAttachedImage: files.length > 0,
|
||||
});
|
||||
const requestId = trimToUndefined(data.requestId);
|
||||
const modelName = trimToUndefined(data.payload?.model?.value);
|
||||
|
||||
const iframeContext: Record<string, unknown> = {};
|
||||
if (requestId) {
|
||||
iframeContext.brainy_request_id = requestId;
|
||||
}
|
||||
if (modelName) {
|
||||
iframeContext.model_name = modelName;
|
||||
}
|
||||
|
||||
return {
|
||||
submitText,
|
||||
files,
|
||||
...(requestId
|
||||
? { iframeContext: { brainy_request_id: requestId } }
|
||||
: {}),
|
||||
...(Object.keys(iframeContext).length > 0 ? { iframeContext } : {}),
|
||||
...(promptPrefix ? { additionalKwargs: { prompt_prefix: promptPrefix } } : {}),
|
||||
};
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Eye,
|
||||
GripVertical,
|
||||
ListOrdered,
|
||||
Loader2,
|
||||
@@ -61,6 +62,7 @@ import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AgentInspectDialog } from "@/components/workspace/agents/agent-inspect-dialog";
|
||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||
import { listAgents } from "@/core/agents";
|
||||
|
||||
@@ -832,6 +834,7 @@ function SeatStrip({
|
||||
const avatarType = getAvatarTypeFor(seat.agent_id, index);
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === total - 1;
|
||||
const [viewOpen, setViewOpen] = useState(false);
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
@@ -877,8 +880,19 @@ function SeatStrip({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 控制:上移 / 下移 / 移除 */}
|
||||
{/* 控制:查看 / 上移 / 下移 / 移除 */}
|
||||
<div className="flex shrink-0 items-center gap-0.5 border-l border-border px-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewOpen(true)}
|
||||
title="查看智能体详情"
|
||||
className={cn(
|
||||
"p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
||||
R_CTRL,
|
||||
)}
|
||||
>
|
||||
<Eye className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMove(-1)}
|
||||
@@ -921,6 +935,12 @@ function SeatStrip({
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentInspectDialog
|
||||
agentId={seat.agent_id}
|
||||
open={viewOpen}
|
||||
onOpenChange={setViewOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user