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 {
|
import {
|
||||||
BotIcon,
|
BotIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
@@ -85,6 +87,10 @@ export function AgentCard({ agent }: AgentCardProps) {
|
|||||||
agent.skills ?? [],
|
agent.skills ?? [],
|
||||||
);
|
);
|
||||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
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
|
// The /api/agents list response intentionally omits `soul` (perf optimization
|
||||||
// — SOUL.md is only read on demand). Fetch the detail row when the edit
|
// — 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 canFavorite = !isOwned;
|
||||||
const isFavorite = !!agent.is_favorite;
|
const isFavorite = !!agent.is_favorite;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSkillsExpanded(false);
|
||||||
|
}, [agent.id, agentSkills.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editOpen) {
|
if (!editOpen) {
|
||||||
return;
|
return;
|
||||||
@@ -382,11 +392,13 @@ export function AgentCard({ agent }: AgentCardProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{agent.description && (
|
<div className="mt-2 min-h-10">
|
||||||
<CardDescription className="mt-2 line-clamp-2 text-sm">
|
{agent.description ? (
|
||||||
{desensitize(agent.description)}
|
<CardDescription className="line-clamp-2 text-sm">
|
||||||
</CardDescription>
|
{desensitize(agent.description)}
|
||||||
)}
|
</CardDescription>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{(agent.tags?.length ?? 0) > 0 && (
|
{(agent.tags?.length ?? 0) > 0 && (
|
||||||
@@ -396,36 +408,73 @@ export function AgentCard({ agent }: AgentCardProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(agent.tool_groups?.length ?? agent.skills?.length ?? 0) > 0 && (
|
{(agent.tool_groups?.length ?? agent.skills?.length ?? 0) > 0 && (
|
||||||
<CardContent className="pt-0 pb-3">
|
<CardContent className="min-w-0 pt-0 pb-3">
|
||||||
<div className="flex flex-wrap gap-1">
|
{(agent.tool_groups?.length ?? 0) > 0 && (
|
||||||
{agent.tool_groups?.map((group) => (
|
<div className="mb-1 flex flex-wrap gap-1">
|
||||||
<Badge
|
{agent.tool_groups?.map((group) => (
|
||||||
key={`tg:${group}`}
|
<Badge
|
||||||
variant="outline"
|
key={`tg:${group}`}
|
||||||
className="text-xs"
|
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)}
|
{agentSkills.map((skill) => (
|
||||||
</Badge>
|
<Badge
|
||||||
))}
|
key={`sk:${skill}`}
|
||||||
{agent.skills?.map((skill) => (
|
variant="secondary"
|
||||||
<Badge
|
className="hover:bg-secondary/70 max-w-full min-w-0 cursor-pointer text-xs"
|
||||||
key={`sk:${skill}`}
|
title={t.settings.skills.viewDetailAction}
|
||||||
variant="secondary"
|
onClick={(e) => {
|
||||||
className="hover:bg-secondary/70 cursor-pointer text-xs"
|
e.stopPropagation();
|
||||||
title={t.settings.skills.viewDetailAction}
|
setDetailSkillName(skill);
|
||||||
onClick={(e) => {
|
}}
|
||||||
e.stopPropagation();
|
>
|
||||||
setDetailSkillName(skill);
|
<span className="truncate">{desensitize(skill)}</span>
|
||||||
}}
|
</Badge>
|
||||||
>
|
))}
|
||||||
{desensitize(skill)}
|
</div>
|
||||||
</Badge>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CardFooter className="mt-auto flex flex-col items-stretch gap-2 pt-3">
|
<CardFooter className="mt-auto flex flex-col items-stretch gap-2 pt-3">
|
||||||
|
|
||||||
{(canEdit || canDelete || isOwned || isAdmin || canFavorite) && (
|
{(canEdit || canDelete || isOwned || isAdmin || canFavorite) && (
|
||||||
<div className="flex flex-wrap items-center justify-end gap-1">
|
<div className="flex flex-wrap items-center justify-end gap-1">
|
||||||
{canFavorite && (
|
{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";
|
"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 { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -464,13 +464,41 @@ const HISTORY_LABELS: Record<string, string> = {
|
|||||||
longTermBackground: "长期背景",
|
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> = {
|
const CATEGORY_COLORS: Record<string, string> = {
|
||||||
preference: "bg-blue-100 text-blue-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-100 text-green-700",
|
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-yellow-100 text-yellow-700",
|
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-100 text-purple-700",
|
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-100 text-orange-700",
|
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-100 text-red-700",
|
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 布局。 */
|
/** V1 记忆只读视图:展示 memory.json 的全部字段,Tab 布局。 */
|
||||||
@@ -484,6 +512,9 @@ function MemoryV1View({
|
|||||||
memory_usage: string;
|
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 ctxEntries = Object.entries(memory.user_context ?? {}).filter(([, v]) => v);
|
||||||
const historyEntries = Object.entries(memory.history ?? {}).filter(([, v]) => v);
|
const historyEntries = Object.entries(memory.history ?? {}).filter(([, v]) => v);
|
||||||
|
|
||||||
@@ -519,37 +550,63 @@ function MemoryV1View({
|
|||||||
暂无事实记录,对话后系统会自动提取。
|
暂无事实记录,对话后系统会自动提取。
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 pt-2">
|
||||||
{memory.facts.map((fact) => (
|
{memory.facts.map((fact) => {
|
||||||
<div key={fact.id} className="rounded-md border p-3 space-y-1.5">
|
const categoryLabel = labelMap[fact.category] ?? fact.category;
|
||||||
<div className="flex items-center gap-2">
|
const Icon = CATEGORY_ICONS[fact.category] ?? PlusIcon;
|
||||||
<span
|
return (
|
||||||
className={`rounded px-1.5 py-0.5 text-xs font-medium ${
|
<div
|
||||||
CATEGORY_COLORS[fact.category] ?? "bg-gray-100 text-gray-700"
|
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"
|
||||||
>
|
>
|
||||||
{fact.category}
|
{/* Subtle top highlight */}
|
||||||
</span>
|
<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="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
|
||||||
<div
|
<div className="space-y-4">
|
||||||
className="h-full rounded-full bg-primary"
|
<div className="flex items-center justify-between gap-2">
|
||||||
style={{ width: `${Math.round(fact.confidence * 100)}%` }}
|
<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>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground shrink-0">
|
|
||||||
{Math.round(fact.confidence * 100)}%
|
{fact.sourceError && (
|
||||||
</span>
|
<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>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -44,6 +51,7 @@ import {
|
|||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from "@/components/ui/empty";
|
} from "@/components/ui/empty";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Item,
|
Item,
|
||||||
ItemActions,
|
ItemActions,
|
||||||
@@ -322,6 +330,8 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
{!isLoading && !error && filteredSkills.length === 0 && (
|
{!isLoading && !error && filteredSkills.length === 0 && (
|
||||||
<EmptySkill onCreateSkill={handleCreateSkill} />
|
<EmptySkill onCreateSkill={handleCreateSkill} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* —— 原纵向列表布局(保留备查)——
|
||||||
{!isLoading && !error &&
|
{!isLoading && !error &&
|
||||||
filteredSkills.length > 0 &&
|
filteredSkills.length > 0 &&
|
||||||
pagedSkills.map((skill) => (
|
pagedSkills.map((skill) => (
|
||||||
@@ -331,7 +341,21 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
isAdmin={isAdmin}
|
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 && (
|
{!isLoading && !error && totalPages > 1 && (
|
||||||
<div className="flex items-center justify-end gap-2 pt-1">
|
<div className="flex items-center justify-end gap-2 pt-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -384,10 +408,12 @@ export function SkillRow({
|
|||||||
skill,
|
skill,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
skill: Skill;
|
skill: Skill;
|
||||||
currentUserId: string | null;
|
currentUserId: string | null;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const setPublished = useSetSkillPublished();
|
const setPublished = useSetSkillPublished();
|
||||||
@@ -473,6 +499,8 @@ export function SkillRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
|
{/* —— 原 Item 列表行布局(保留备查)——
|
||||||
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
|
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
|
||||||
<ItemContent
|
<ItemContent
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
@@ -640,7 +668,205 @@ export function SkillRow({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ItemActions>
|
</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}>
|
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -17,20 +17,22 @@ export function TagChip({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const color = resolveTagColor(tag.color);
|
const color = resolveTagColor(tag.color);
|
||||||
|
const label = desensitize(tag.name);
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
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,
|
color.chip,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
title={label}
|
||||||
>
|
>
|
||||||
{desensitize(tag.name)}
|
<span className="min-w-0 truncate">{label}</span>
|
||||||
{onRemove && (
|
{onRemove && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
className="hover:opacity-70"
|
className="shrink-0 hover:opacity-70"
|
||||||
aria-label={`remove ${tag.name}`}
|
aria-label={`remove ${tag.name}`}
|
||||||
>
|
>
|
||||||
<XIcon className="size-3" />
|
<XIcon className="size-3" />
|
||||||
@@ -50,7 +52,7 @@ export function TagChipList({
|
|||||||
}) {
|
}) {
|
||||||
if (!tags || tags.length === 0) return null;
|
if (!tags || tags.length === 0) return null;
|
||||||
return (
|
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) => (
|
{tags.map((tag) => (
|
||||||
<TagChip key={tag.name} tag={tag} />
|
<TagChip key={tag.name} tag={tag} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -621,10 +621,15 @@ export function useThreadStream({
|
|||||||
lockThreadRagMode(threadId);
|
lockThreadRagMode(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await thread.submit(
|
const mergedContext = {
|
||||||
{
|
...context,
|
||||||
messages: [
|
...extraContext,
|
||||||
{
|
};
|
||||||
|
|
||||||
|
await thread.submit(
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
type: "human",
|
type: "human",
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -642,32 +647,33 @@ export function useThreadStream({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
streamSubgraphs: true,
|
streamSubgraphs: true,
|
||||||
streamResumable: true,
|
streamResumable: true,
|
||||||
config: {
|
config: {
|
||||||
recursion_limit: 1000,
|
recursion_limit: 1000,
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
...extraContext,
|
...mergedContext,
|
||||||
...context,
|
thinking_enabled: mergedContext.mode !== "flash",
|
||||||
thinking_enabled: context.mode !== "flash",
|
is_plan_mode:
|
||||||
is_plan_mode: context.mode === "pro" || context.mode === "ultra",
|
mergedContext.mode === "pro" || mergedContext.mode === "ultra",
|
||||||
subagent_enabled: context.mode === "ultra",
|
subagent_enabled: mergedContext.mode === "ultra",
|
||||||
reasoning_effort:
|
reasoning_effort:
|
||||||
context.reasoning_effort ??
|
mergedContext.reasoning_effort ??
|
||||||
(context.mode === "ultra"
|
(mergedContext.mode === "ultra"
|
||||||
? "high"
|
? "high"
|
||||||
: context.mode === "pro"
|
: mergedContext.mode === "pro"
|
||||||
? "medium"
|
? "medium"
|
||||||
: context.mode === "thinking"
|
: mergedContext.mode === "thinking"
|
||||||
? "low"
|
? "low"
|
||||||
: undefined),
|
: undefined),
|
||||||
memory_injection_enabled: context.memory_injection_enabled ?? true,
|
memory_injection_enabled:
|
||||||
rag_mode_enabled: ragModeEnabled,
|
mergedContext.memory_injection_enabled ?? true,
|
||||||
thread_id: threadId,
|
rag_mode_enabled: ragModeEnabled,
|
||||||
},
|
thread_id: threadId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||||
|
|||||||
@@ -102,17 +102,99 @@ import {
|
|||||||
// Panel IDs used as keys in the v4 Layout map
|
// Panel IDs used as keys in the v4 Layout map
|
||||||
const WRITING_ID = 'aw-writing';
|
const WRITING_ID = 'aw-writing';
|
||||||
const DRAFT_ID = 'aw-draft';
|
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)
|
// Match the original canvas page proportions (chat=35, artifact=65)
|
||||||
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 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」,帮用户聊清并回填左侧表单;
|
* 写作配置表单面板(写作助手回填的目标),抽出复用:idle 时在右栏,开始写作后在左栏。
|
||||||
* 一旦开始写作(status !== 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();
|
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({
|
export default function AIWritingPage({
|
||||||
@@ -120,8 +202,6 @@ export default function AIWritingPage({
|
|||||||
}: {
|
}: {
|
||||||
hideAdvancedFields?: boolean;
|
hideAdvancedFields?: boolean;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const layoutRef = useRef<GroupImperativeHandle>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasThreadProvider>
|
<CanvasThreadProvider>
|
||||||
<CanvasGraphProvider>
|
<CanvasGraphProvider>
|
||||||
@@ -131,37 +211,7 @@ export default function AIWritingPage({
|
|||||||
<TooltipProvider delayDuration={300}>
|
<TooltipProvider delayDuration={300}>
|
||||||
<div className="h-full flex flex-col overflow-hidden bg-background">
|
<div className="h-full flex flex-col overflow-hidden bg-background">
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<ResizablePanelGroup
|
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -220,8 +220,10 @@ export default function IframeChatPage() {
|
|||||||
prompt: data.payload?.prompt,
|
prompt: data.payload?.prompt,
|
||||||
pageContext: data.payload?.pageContext,
|
pageContext: data.payload?.pageContext,
|
||||||
screenshot: data.payload?.screenshot,
|
screenshot: data.payload?.screenshot,
|
||||||
|
model: data.payload?.model,
|
||||||
requestId: data.requestId,
|
requestId: data.requestId,
|
||||||
filesCount: files.length,
|
filesCount: files.length,
|
||||||
|
iframeContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!submitText && files.length === 0) return;
|
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]?.filename, "shot.jpg");
|
||||||
assert.equal(result.files[0]?.mediaType, "image/jpeg");
|
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[];
|
images?: BrainyScreenshotImage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BrainyModelSelection = {
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type BrainyChatSubmitMessage = {
|
export type BrainyChatSubmitMessage = {
|
||||||
source?: string;
|
source?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -29,6 +34,7 @@ export type BrainyChatSubmitMessage = {
|
|||||||
prompt?: string;
|
prompt?: string;
|
||||||
pageContext?: BrainyPageContext;
|
pageContext?: BrainyPageContext;
|
||||||
screenshot?: BrainyScreenshot;
|
screenshot?: BrainyScreenshot;
|
||||||
|
model?: BrainyModelSelection;
|
||||||
};
|
};
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
};
|
};
|
||||||
@@ -261,13 +267,20 @@ export function parseBrainyChatSubmitMessage(
|
|||||||
hasAttachedImage: files.length > 0,
|
hasAttachedImage: files.length > 0,
|
||||||
});
|
});
|
||||||
const requestId = trimToUndefined(data.requestId);
|
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 {
|
return {
|
||||||
submitText,
|
submitText,
|
||||||
files,
|
files,
|
||||||
...(requestId
|
...(Object.keys(iframeContext).length > 0 ? { iframeContext } : {}),
|
||||||
? { iframeContext: { brainy_request_id: requestId } }
|
|
||||||
: {}),
|
|
||||||
...(promptPrefix ? { additionalKwargs: { prompt_prefix: promptPrefix } } : {}),
|
...(promptPrefix ? { additionalKwargs: { prompt_prefix: promptPrefix } } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
Bot,
|
Bot,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
Eye,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
ListOrdered,
|
ListOrdered,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -61,6 +62,7 @@ import { CSS } from "@dnd-kit/utilities";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||||
import { listAgents } from "@/core/agents";
|
import { listAgents } from "@/core/agents";
|
||||||
|
|
||||||
@@ -832,6 +834,7 @@ function SeatStrip({
|
|||||||
const avatarType = getAvatarTypeFor(seat.agent_id, index);
|
const avatarType = getAvatarTypeFor(seat.agent_id, index);
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === total - 1;
|
const isLast = index === total - 1;
|
||||||
|
const [viewOpen, setViewOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
@@ -877,8 +880,19 @@ function SeatStrip({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 控制:上移 / 下移 / 移除 */}
|
{/* 控制:查看 / 上移 / 下移 / 移除 */}
|
||||||
<div className="flex shrink-0 items-center gap-0.5 border-l border-border px-2">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onMove(-1)}
|
onClick={() => onMove(-1)}
|
||||||
@@ -921,6 +935,12 @@ function SeatStrip({
|
|||||||
<X className="size-3.5" />
|
<X className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AgentInspectDialog
|
||||||
|
agentId={seat.agent_id}
|
||||||
|
open={viewOpen}
|
||||||
|
onOpenChange={setViewOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user