Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
This commit is contained in:
commit
6efe1b80c5
10 files changed
+603
-67
No files matched your search
@@ -0,0 +1,299 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
CheckIcon,
|
||||
InfoIcon,
|
||||
MessageCircleQuestionIcon,
|
||||
PanelRightCloseIcon,
|
||||
PanelRightOpenIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
SparklesIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
||||
import { Tooltip } from "@/components/workspace/tooltip";
|
||||
import { type Agent } from "@/core/agents";
|
||||
import { useUpdateAgent } from "@/core/agents/hooks";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Collapsible right-hand panel on the agent chat page that surfaces the agent's
|
||||
* profile (name / description / skills) plus a list of frequently-asked
|
||||
* questions ("常用问题"). Clicking a question sends it as a message. The agent's
|
||||
* owner (or an admin) can edit the question list inline.
|
||||
*/
|
||||
export function AgentInfoSidebar({
|
||||
agent,
|
||||
agentName,
|
||||
onAskQuestion,
|
||||
disabled,
|
||||
}: {
|
||||
agent: Agent | null | undefined;
|
||||
agentName: string;
|
||||
onAskQuestion: (text: string) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||
const { skills } = useSkills();
|
||||
const updateAgent = useUpdateAgent();
|
||||
|
||||
const displayName = desensitize(agent?.name ?? agentName);
|
||||
const description = agent?.description
|
||||
? desensitize(agent.description)
|
||||
: undefined;
|
||||
|
||||
// Owner / admin gate for inline editing of the common-questions list.
|
||||
const storedAuth = useMemo(() => getStoredAuth(), []);
|
||||
const canEdit = useMemo(() => {
|
||||
if (!agent) return false;
|
||||
if (storedAuth?.system_role === "admin") return true;
|
||||
return agent.user_id != null && agent.user_id === storedAuth?.user_id;
|
||||
}, [agent, storedAuth]);
|
||||
|
||||
// Map a stored skill name to its Chinese display label (falls back to name).
|
||||
const skillLabel = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const skill of skills) {
|
||||
map.set(skill.name, skill.name_zh?.trim() ? skill.name_zh : skill.name);
|
||||
}
|
||||
return (name: string) => map.get(name) ?? name;
|
||||
}, [skills]);
|
||||
|
||||
const questions = useMemo(
|
||||
() => (agent?.common_questions ?? []).filter((q) => q.trim().length > 0),
|
||||
[agent?.common_questions],
|
||||
);
|
||||
|
||||
// --- Inline editing state ---
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [drafts, setDrafts] = useState<string[]>([]);
|
||||
|
||||
const startEditing = () => {
|
||||
setDrafts(questions.length > 0 ? [...questions] : [""]);
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const cancelEditing = () => {
|
||||
setEditing(false);
|
||||
setDrafts([]);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!agent) return;
|
||||
const cleaned = drafts.map((q) => q.trim()).filter((q) => q.length > 0);
|
||||
try {
|
||||
await updateAgent.mutateAsync({
|
||||
id: agent.id,
|
||||
request: { common_questions: cleaned },
|
||||
});
|
||||
toast.success("常用问题已保存");
|
||||
setEditing(false);
|
||||
setDrafts([]);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
// Leaving edit mode whenever the panel collapses keeps the state tidy.
|
||||
useEffect(() => {
|
||||
if (collapsed && editing) cancelEditing();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collapsed]);
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<div className="flex shrink-0 flex-col items-center border-l pt-14">
|
||||
<Tooltip content="智能体简介">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setCollapsed(false)}
|
||||
aria-label="展开智能体简介"
|
||||
>
|
||||
<PanelRightOpenIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="bg-card/40 flex w-72 shrink-0 flex-col border-l pt-12">
|
||||
<div className="flex items-center justify-between border-b px-3 py-2">
|
||||
<span className="text-sm font-semibold">智能体简介</span>
|
||||
<Tooltip content="收起">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setCollapsed(true)}
|
||||
aria-label="收起智能体简介"
|
||||
>
|
||||
<PanelRightCloseIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-5 overflow-y-auto p-4">
|
||||
{/* Identity */}
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<BotIcon className="text-primary h-6 w-6" />
|
||||
</div>
|
||||
<div className="text-base font-bold" title={displayName}>
|
||||
{displayName}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Skills */}
|
||||
{agent?.skills && agent.skills.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
|
||||
<SparklesIcon className="h-3.5 w-3.5" />
|
||||
技能
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{agent.skills.map((name) => (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
onClick={() => setDetailSkillName(name)}
|
||||
title="查看技能详情"
|
||||
className="border-border bg-background hover:bg-accent inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs transition-colors"
|
||||
>
|
||||
<span className="max-w-[150px] truncate">
|
||||
{skillLabel(name)}
|
||||
</span>
|
||||
<InfoIcon className="text-muted-foreground h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Common questions */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
|
||||
<MessageCircleQuestionIcon className="h-3.5 w-3.5" />
|
||||
常用问题
|
||||
</div>
|
||||
{canEdit && !editing && (
|
||||
<Tooltip content="编辑常用问题">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={startEditing}
|
||||
aria-label="编辑常用问题"
|
||||
>
|
||||
<PencilIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div className="space-y-2">
|
||||
{drafts.map((value, index) => (
|
||||
<div key={index} className="flex items-center gap-1">
|
||||
<Input
|
||||
value={value}
|
||||
placeholder={`问题 ${index + 1}`}
|
||||
onChange={(e) => {
|
||||
const next = [...drafts];
|
||||
next[index] = e.currentTarget.value;
|
||||
setDrafts(next);
|
||||
}}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() =>
|
||||
setDrafts(drafts.filter((_, i) => i !== index))
|
||||
}
|
||||
aria-label="删除"
|
||||
>
|
||||
<XIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => setDrafts([...drafts, ""])}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5" /> 添加问题
|
||||
</Button>
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => void handleSave()}
|
||||
disabled={updateAgent.isPending}
|
||||
>
|
||||
<CheckIcon className="h-3.5 w-3.5" /> 保存
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={cancelEditing}
|
||||
disabled={updateAgent.isPending}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : questions.length > 0 ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{questions.map((question, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onAskQuestion(question)}
|
||||
title={question}
|
||||
className={cn(
|
||||
"border-border bg-background hover:bg-accent w-full rounded-md border px-2.5 py-1.5 text-left text-xs transition-colors",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{canEdit ? "还没有常用问题,点击上方编辑添加。" : "暂无常用问题。"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SkillDetailDialog
|
||||
skillName={detailSkillName}
|
||||
open={detailSkillName !== null}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) setDetailSkillName(null);
|
||||
}}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export interface Agent {
|
||||
model?: string | null;
|
||||
tool_groups?: string[] | null;
|
||||
skills?: string[] | null;
|
||||
/** Frequently-asked questions (常用问题) shown in the agent chat sidebar. */
|
||||
common_questions?: string[] | null;
|
||||
tags?: Tag[];
|
||||
featured_order?: number | null;
|
||||
/** Whether the current user has favorited (收藏) this agent. */
|
||||
@@ -46,6 +48,7 @@ export interface CreateAgentRequest {
|
||||
skills?: string[] | null;
|
||||
model?: string | null;
|
||||
tool_groups?: string[] | null;
|
||||
common_questions?: string[] | null;
|
||||
}
|
||||
|
||||
export interface UpdateAgentRequest {
|
||||
@@ -57,4 +60,5 @@ export interface UpdateAgentRequest {
|
||||
skills?: string[] | null;
|
||||
model?: string | null;
|
||||
tool_groups?: string[] | null;
|
||||
common_questions?: string[] | null;
|
||||
}
|
||||
@@ -434,7 +434,17 @@ export function findToolCallResult(
|
||||
}
|
||||
|
||||
export function isHiddenFromUIMessage(message: Message) {
|
||||
return message.additional_kwargs?.hide_from_ui === true;
|
||||
if (message.additional_kwargs?.hide_from_ui === true) {
|
||||
return true;
|
||||
}
|
||||
// Conversation-summary messages are context for the model only — never shown
|
||||
// to the user. New summaries are injected transiently (never persisted), but
|
||||
// threads compacted by older builds still carry a persisted `name: "summary"`
|
||||
// HumanMessage, so hide those too.
|
||||
if (message.name === "summary") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AgentInfoSidebar } from "@/components/workspace/agent-info-sidebar";
|
||||
import { AgentWelcome } from "@/components/workspace/agent-welcome";
|
||||
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
||||
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
|
||||
@@ -285,6 +286,16 @@ export default function AgentChatPage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<AgentInfoSidebar
|
||||
agent={agent}
|
||||
agentName={agentDisplayName}
|
||||
disabled={
|
||||
thread.isLoading ||
|
||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
|
||||
}
|
||||
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
|
||||
/>
|
||||
</div>
|
||||
</ChatBox>
|
||||
</ThreadContext.Provider>
|
||||
|
||||
Reference in new issue
Block a user