Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd
This commit is contained in:
commit
8f88427d4e
19 files changed
+751
-76
No files matched your search
@@ -2,8 +2,9 @@
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
GlobeIcon,
|
||||
InfoIcon,
|
||||
LockIcon,
|
||||
MessageSquareIcon,
|
||||
SquarePenIcon,
|
||||
TagsIcon,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
||||
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
||||
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
||||
import { useDeleteAgent, useUpdateAgent } from "@/core/agents";
|
||||
@@ -62,6 +64,7 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>(
|
||||
agent.skills ?? [],
|
||||
);
|
||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||
|
||||
const storedAuth = useMemo(() => getStoredAuth(), []);
|
||||
const currentUserId = storedAuth?.user_id ?? null;
|
||||
@@ -204,6 +207,7 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
|
||||
title="该智能体已对所有用户可见"
|
||||
>
|
||||
<GlobeIcon className="size-3" />
|
||||
已发布
|
||||
</Badge>
|
||||
)}
|
||||
@@ -238,7 +242,12 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
<Badge
|
||||
key={`sk:${skill}`}
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
className="hover:bg-secondary/70 cursor-pointer text-xs"
|
||||
title={t.settings.skills.viewDetailAction}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDetailSkillName(skill);
|
||||
}}
|
||||
>
|
||||
{skill}
|
||||
</Badge>
|
||||
@@ -275,9 +284,9 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
title={agent.published ? "取消发布" : "发布"}
|
||||
>
|
||||
{agent.published ? (
|
||||
<EyeOffIcon className="h-3.5 w-3.5" />
|
||||
<LockIcon className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<EyeIcon className="h-3.5 w-3.5" />
|
||||
<GlobeIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -353,17 +362,31 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
{enabledSkills.map((skill) => {
|
||||
const checked = selectedSkills.includes(skill.name);
|
||||
return (
|
||||
<Button
|
||||
<div
|
||||
key={skill.name}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => toggleSkill(skill.name)}
|
||||
title={skill.description}
|
||||
className="h-8"
|
||||
className="inline-flex max-w-full items-center"
|
||||
>
|
||||
{skill.name}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => toggleSkill(skill.name)}
|
||||
title={skill.description}
|
||||
className="h-8 max-w-[200px] rounded-r-none"
|
||||
>
|
||||
<span className="truncate">{skill.name}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => setDetailSkillName(skill.name)}
|
||||
title={t.settings.skills.viewDetailAction}
|
||||
className="h-8 w-7 rounded-l-none border-l-0"
|
||||
>
|
||||
<InfoIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -445,6 +468,14 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
currentTags={agent.tags ?? []}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SkillDetailDialog
|
||||
skillName={detailSkillName}
|
||||
open={detailSkillName !== null}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) setDetailSkillName(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
ArchiveIcon,
|
||||
CheckCircle2,
|
||||
ClockIcon,
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
FileArchive,
|
||||
GlobeIcon,
|
||||
Loader2,
|
||||
LockIcon,
|
||||
PinIcon,
|
||||
PinOffIcon,
|
||||
SparklesIcon,
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
ItemDescription,
|
||||
} from "@/components/ui/item";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
||||
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
||||
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||
@@ -58,7 +59,6 @@ import {
|
||||
useInstallSkillUpload,
|
||||
useSetSkillPinned,
|
||||
useSetSkillPublished,
|
||||
useSkillContent,
|
||||
useSkills,
|
||||
useValidateSkillUpload,
|
||||
} from "@/core/skills/hooks";
|
||||
@@ -358,6 +358,7 @@ function SkillRow({
|
||||
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>
|
||||
)}
|
||||
@@ -453,9 +454,9 @@ function SkillRow({
|
||||
}
|
||||
>
|
||||
{skill.published ? (
|
||||
<EyeOffIcon className="size-3.5" />
|
||||
<LockIcon className="size-3.5" />
|
||||
) : (
|
||||
<EyeIcon className="size-3.5" />
|
||||
<GlobeIcon className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -508,7 +509,7 @@ function SkillRow({
|
||||
/>
|
||||
)}
|
||||
<SkillDetailDialog
|
||||
skill={skill}
|
||||
skillName={skill.name}
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
/>
|
||||
@@ -516,46 +517,6 @@ function SkillRow({
|
||||
);
|
||||
}
|
||||
|
||||
function SkillDetailDialog({
|
||||
skill,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
skill: Skill;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { data, isLoading, error } = useSkillContent(open ? skill.name : null);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-h-[80vh] flex-col gap-3 sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{skill.name}</DialogTitle>
|
||||
<DialogDescription>{skill.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="bg-muted/40 min-h-0 flex-1 overflow-auto rounded-md border">
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground flex items-center justify-center gap-2 py-16 text-sm">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{t.common.loading}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-destructive p-4 text-sm">
|
||||
{error instanceof Error ? error.message : String(error)}
|
||||
</div>
|
||||
) : (
|
||||
<pre className="text-foreground/90 whitespace-pre-wrap wrap-break-word p-4 font-mono text-xs leading-relaxed">
|
||||
{data?.content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptySkill({ onCreateSkill }: { onCreateSkill: () => void }) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, PencilIcon } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
useSetSkillDetail,
|
||||
useSkill,
|
||||
useSkillContent,
|
||||
} from "@/core/skills/hooks";
|
||||
import type { Skill } from "@/core/skills/type";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
|
||||
/**
|
||||
* Whether the current user may edit a skill's detail text.
|
||||
*
|
||||
* Admins can edit any skill; the publisher (owner of a custom skill) can edit
|
||||
* their own. Built-in skills have no human owner, so only admins qualify.
|
||||
* Mirrors the backend authz in `PUT /api/skills/{name}/detail`.
|
||||
*/
|
||||
function canEditSkillDetail(skill: Skill | null | undefined): boolean {
|
||||
if (!skill) return false;
|
||||
const auth = getStoredAuth();
|
||||
if (!auth) return false;
|
||||
if (auth.system_role === "admin") return true;
|
||||
return (
|
||||
skill.category === "custom" &&
|
||||
!!skill.owner_user_id &&
|
||||
skill.owner_user_id === auth.user_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable skill detail dialog. Given a skill name it fetches the skill
|
||||
* metadata (including the admin/owner-authored `detail`) and the raw SKILL.md
|
||||
* source, and lets authorized users edit the detail inline.
|
||||
*/
|
||||
export function SkillDetailDialog({
|
||||
skillName,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
skillName: string | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const tx = t.settings.skills;
|
||||
// Only fetch while the dialog is open to avoid background requests.
|
||||
const activeName = open ? skillName : null;
|
||||
const { data: skill, isLoading: isSkillLoading } = useSkill(activeName);
|
||||
const {
|
||||
data: contentData,
|
||||
isLoading: isContentLoading,
|
||||
error: contentError,
|
||||
} = useSkillContent(activeName);
|
||||
const setDetail = useSetSkillDetail();
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [showSource, setShowSource] = useState(false);
|
||||
|
||||
// Reset transient UI whenever the dialog closes or switches to another skill.
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setEditing(false);
|
||||
setShowSource(false);
|
||||
}
|
||||
}, [open, skillName]);
|
||||
|
||||
const canEdit = useMemo(() => canEditSkillDetail(skill), [skill]);
|
||||
|
||||
function startEditing() {
|
||||
setDraft(skill?.detail ?? "");
|
||||
setEditing(true);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!skill) return;
|
||||
try {
|
||||
await setDetail.mutateAsync({ skillName: skill.name, detail: draft });
|
||||
toast.success(tx.detailSaveSuccess);
|
||||
setEditing(false);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-h-[80vh] flex-col gap-3 sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{skill?.name ?? skillName ?? ""}</DialogTitle>
|
||||
<DialogDescription>{skill?.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-auto pr-1">
|
||||
{/* Detail — human-authored long-form introduction */}
|
||||
<section className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">{tx.detailSectionTitle}</h3>
|
||||
{canEdit && !editing && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7"
|
||||
onClick={startEditing}
|
||||
>
|
||||
<PencilIcon className="size-3.5" />
|
||||
{tx.detailEditAction}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{editing ? (
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
placeholder={tx.detailPlaceholder}
|
||||
className="max-h-[320px] min-h-40 resize-y overflow-y-auto text-sm"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setEditing(false)}
|
||||
disabled={setDetail.isPending}
|
||||
>
|
||||
{t.common.cancel}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleSave()}
|
||||
disabled={setDetail.isPending}
|
||||
>
|
||||
{setDetail.isPending && (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
)}
|
||||
{t.common.save}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : isSkillLoading ? (
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{t.common.loading}
|
||||
</div>
|
||||
) : skill?.detail ? (
|
||||
<div className="bg-muted/40 rounded-md border p-3 text-sm">
|
||||
<MarkdownContent
|
||||
content={skill.detail}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{tx.detailEmpty}
|
||||
{!canEdit ? ` ${tx.detailAdminOnlyHint}` : ""}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* SKILL.md source — collapsed by default */}
|
||||
<section className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-foreground text-sm font-medium"
|
||||
onClick={() => setShowSource((v) => !v)}
|
||||
>
|
||||
{showSource ? "▾ " : "▸ "}
|
||||
{tx.contentSectionTitle}
|
||||
</button>
|
||||
{showSource && (
|
||||
<div className="bg-muted/40 overflow-auto rounded-md border">
|
||||
{isContentLoading ? (
|
||||
<div className="text-muted-foreground flex items-center justify-center gap-2 py-10 text-sm">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{t.common.loading}
|
||||
</div>
|
||||
) : contentError ? (
|
||||
<div className="text-destructive p-4 text-sm">
|
||||
{contentError instanceof Error
|
||||
? contentError.message
|
||||
: String(contentError)}
|
||||
</div>
|
||||
) : (
|
||||
<pre className="text-foreground/90 max-h-[320px] overflow-auto whitespace-pre-wrap wrap-break-word p-4 font-mono text-xs leading-relaxed">
|
||||
{contentData?.content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user