feat: 新增表格文件预览,优化知识图谱与 Agent 详情交互

- 新增 spreadsheet-preview 组件及 xlsx 解析工具,支持产物中的表格文件预览
- 优化知识图谱画布、详情面板及样式
- 调整 Agent 详情面板、聊天与消息列表交互

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-11 17:43:09 +08:00
1 parent 5daa4b3e20
commit 7d61bc09bc
18 files changed
+1092 -208

No files matched your search

+28 -43
View File
@@ -1,6 +1,6 @@
"use client";
import { BotIcon, MessageCircleQuestionIcon, PlusSquare } from "lucide-react";
import { BotIcon, PlusSquare } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useRouteBase } from "@/core/route-base";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -62,7 +62,6 @@ export default function AgentChatPage() {
const agentDisplayName = desensitize(
agent?.name ?? (isAgentLoading ? t.common.loading : ""),
);
// 常用问题 shown as a list beside the input box on the landing screen.
const commonQuestions = useMemo(
() => normalizeAgentQuestions(agent?.common_questions),
[agent?.common_questions],
@@ -181,9 +180,9 @@ export default function AgentChatPage() {
separate flex sibling, so nothing in the main column can ever bleed
over it. */}
<div className="flex size-full min-h-0 overflow-hidden">
{/* ── Left: main conversation column ───────────────────────────── */}
{/* Left: main conversation column */}
<main className="flex min-h-0 min-w-0 flex-1 flex-col">
{/* Header — in normal flow (not absolute), fixed height. */}
{/* Header in normal flow, fixed height. */}
<header className="bg-card/90 border-border/70 flex h-12 shrink-0 items-center gap-2 border-b px-4 backdrop-blur-md">
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
<BotIcon className="text-primary h-3.5 w-3.5" />
@@ -223,9 +222,9 @@ export default function AgentChatPage() {
</header>
{isLandingState ? (
/* ── home: centered hero + suggested questions + input ──────
/* Home: centered hero, suggested questions, and input.
Everything lives inside the scrollable main column. No fixed,
no absolute, no viewport centering — so the avatar can't touch
no absolute, no viewport centering, so the avatar can't touch
the header and the input can't cover the sidebar. */
<div className="flex min-h-0 flex-1 flex-col items-center overflow-y-auto px-6 pb-10 pt-16 sm:pt-24">
<AgentWelcome
@@ -233,42 +232,28 @@ export default function AgentChatPage() {
agentName={agentDisplayName}
className="mb-7"
/>
{/* 常用问题 sit as a list to the LEFT of the input box (not as
floating pills above it). On narrow screens the list stacks
above the input instead of squeezing it. */}
<div
className={cn(
"flex w-full flex-col gap-3 sm:flex-row sm:items-end sm:gap-4",
commonQuestions.length > 0
? "max-w-(--container-width-lg)"
: "max-w-(--container-width-md)",
)}
>
<div className="w-full max-w-(--container-width-md)">
{commonQuestions.length > 0 && (
<div className="border-border/60 bg-card/60 w-full shrink-0 rounded-xl border p-3 sm:w-64">
<div className="text-muted-foreground mb-2 flex items-center gap-1.5 text-xs font-medium">
<MessageCircleQuestionIcon className="size-3.5" />
常用问题
</div>
<div className="flex max-h-44 flex-col gap-1.5 overflow-y-auto">
{commonQuestions.map((question, index) => (
<button
key={index}
type="button"
disabled={isDemoMode}
onClick={() =>
handleSubmit({ text: question.prompt, files: [] })
}
title={desensitize(question.prompt)}
className={cn(
"border-border/60 bg-background/60 hover:bg-accent hover:border-border w-full truncate rounded-lg border px-3 py-2 text-left text-sm transition-colors",
"disabled:cursor-not-allowed disabled:opacity-50",
)}
>
<div className="mb-2 flex max-h-20 flex-wrap items-start justify-start gap-2 overflow-y-auto px-1">
{commonQuestions.map((question, index) => (
<button
key={index}
type="button"
disabled={isDemoMode}
onClick={() =>
handleSubmit({ text: question.prompt, files: [] })
}
title={desensitize(question.prompt)}
className={cn(
"border-border/70 bg-background/85 hover:bg-accent hover:border-border max-w-full rounded-full border px-3 py-1.5 text-left text-sm leading-5 shadow-sm transition-colors",
"disabled:cursor-not-allowed disabled:opacity-50",
)}
>
<span className="block max-w-[220px] truncate">
{desensitize(question.title)}
</button>
))}
</div>
</span>
</button>
))}
</div>
)}
<div className="min-w-0 flex-1">
@@ -298,7 +283,7 @@ export default function AgentChatPage() {
</div>
</div>
) : (
/* ── conversation (active / history): messages scroll, input
/* Conversation: messages scroll, input
docked at the bottom of the main column only. */
<div className="flex min-h-0 flex-1 flex-col">
<MessageList
@@ -347,8 +332,7 @@ export default function AgentChatPage() {
)}
</main>
{/* ── Right: agent detail panel (profile + inline editing for
admins / owners; read-only for everyone else) ───────────────── */}
{/* Right: agent detail panel. */}
<AgentDetailPanel
agent={agent}
agentName={agentDisplayName}
@@ -360,3 +344,4 @@ export default function AgentChatPage() {
</ThreadContext.Provider>
);
}
+14 -9
View File
@@ -28,10 +28,12 @@ import {
getNodeTitle,
} from "@/components/knowledge-graph/graphUtils";
import "@/components/knowledge-graph/graphStyles.css";
import { useTheme } from "@/contexts/ThemeContext";
import { useKnowledgeGraph, useKnowledgeNote } from "@/core/knowledge/hooks";
export default function KnowledgeGraphPage() {
const navigate = useNavigate();
const { theme } = useTheme();
const pageRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<GraphCanvasHandle>(null);
@@ -54,7 +56,7 @@ export default function KnowledgeGraphPage() {
const [showDetail, setShowDetail] = useState(false);
const [zoomLevel, setZoomLevel] = useState(1);
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
const [graphMode, setGraphMode] = useState<GraphMode>("explore");
const [graphMode, setGraphMode] = useState<GraphMode>("full");
const [exploreRootId, setExploreRootId] = useState<string | null>(null);
const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(new Set());
const [visibleNodeIds, setVisibleNodeIds] = useState<Set<string>>(new Set());
@@ -269,14 +271,16 @@ export default function KnowledgeGraphPage() {
return matches;
}, [searchKeyword, filteredNodes]);
const topNodes = useMemo(
() =>
[...allNodes]
.map((node) => ({ node, degree: degrees.get(node.id) ?? 0 }))
.sort((a, b) => b.degree - a.degree)
.slice(0, 8),
[allNodes, degrees],
);
const topNodes = useMemo(() => {
// 搜索时核心节点榜单只保留命中的节点,与画布高亮保持一致。
const pool = searchMatchIds
? filteredNodes.filter((node) => searchMatchIds.has(node.id))
: allNodes;
return pool
.map((node) => ({ node, degree: degrees.get(node.id) ?? 0 }))
.sort((a, b) => b.degree - a.degree)
.slice(0, 8);
}, [allNodes, filteredNodes, searchMatchIds, degrees]);
const selectedNode = selectedNodeId ? (nodeById.get(selectedNodeId) ?? null) : null;
const selectedNeighbors = useMemo(() => {
@@ -633,6 +637,7 @@ export default function KnowledgeGraphPage() {
searchMatchIds={searchMatchIds}
layoutType={layoutType}
showLabels={showLabels}
theme={theme}
onSelectNode={handleSelectNode}
onTooltip={setTooltip}
onZoomChange={setZoomLevel}
+10 -2
View File
@@ -433,14 +433,22 @@ export default function NewAgentPage() {
)}
</div>
<PromptInput
onSubmit={({ text }) => void handleChatSubmit(text)}
onSubmit={({ text }) => {
if (thread.isLoading) {
void thread.stop();
return;
}
void handleChatSubmit(text);
}}
>
<PromptInputTextarea
placeholder="直接告诉模型你想要什么样的智能体,或回答模型的问题"
disabled={thread.isLoading}
/>
<PromptInputFooter className="justify-end">
<PromptInputSubmit disabled={thread.isLoading} />
<PromptInputSubmit
status={thread.isLoading ? "streaming" : "ready"}
/>
</PromptInputFooter>
</PromptInput>
</div>