Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 170 additions & 24 deletions frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type React from "react";
import { useEffect, useState } from "react";
import { Table, Badge, Button, Breadcrumb, Tooltip, App, Card, Input, Empty, Spin } from "antd";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import {
DeleteOutlined,
EditOutlined,
Expand All @@ -18,30 +22,41 @@ import {
queryKnowledgeBaseByIdUsingGet,
queryKnowledgeBaseFilesUsingGet,
retrieveKnowledgeBaseContent,
fetchKnowledgeGraph,
queryKnowledgeBase,
} from "../knowledge-base.api";
import useFetchData from "@/hooks/useFetchData";
import AddDataDialog from "../components/AddDataDialog";
import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
import KnowledgeGraphView, { GraphEntitySelection } from "../components/KnowledgeGraphView";
import { Network } from "lucide-react";
import { useTranslation } from "react-i18next";

interface StatisticItem {
icon?: React.ReactNode;
label: string;
value: string | number;
}
interface RagChunk {
// Use UnifiedSearchResult from model - flat structure from backend
// Backend returns: { id, text, score, metadata, resultType, knowledgeBaseId, knowledgeBaseName }
interface RecallResult {
id: string;
text: string;
metadata: string;
}
interface RecallResult {
score: number;
entity: RagChunk;
id?: string | object;
primaryKey?: string;
metadata: Record<string, any>;
resultType?: string;
knowledgeBaseId?: string;
knowledgeBaseName?: string;
}

function squashSoftLineBreaksOutsideFences(markdown: string): string {
if (!markdown) return "";
const parts = markdown.split(/(```[\s\S]*?```)/g);
return parts
.map((part) => {
if (part.startsWith("```")) return part;
// keep paragraph breaks (\n\n), but squash single newlines into spaces
return part.replace(/([^\n])\n(?!\n)/g, "$1 ");
})
.join("");
}

const KnowledgeBaseDetailPage: React.FC = () => {
Expand Down Expand Up @@ -153,7 +168,7 @@ const KnowledgeBaseDetailPage: React.FC = () => {
setGraphLoading(true);
setGraphSelection(null);
try {
const { data } = await fetchKnowledgeGraph({ knowledge_base_id: knowledgeBase.id, query: "*" });
const { data } = await queryKnowledgeBase({ knowledge_base_id: knowledgeBase.id, query: "*" });
setGraphData({ nodes: data?.nodes ?? [], edges: data?.edges ?? [] });
} catch {
setGraphData({ nodes: [], edges: [] });
Expand All @@ -179,15 +194,6 @@ const KnowledgeBaseDetailPage: React.FC = () => {
};

type DetailOperation = NonNullable<React.ComponentProps<typeof DetailHeader>["operations"][number]>;
const graphOperation: DetailOperation | null = knowledgeBase?.type === KBType.GRAPH
? {
key: "graph",
label: t("knowledgeBase.detail.graph.title"),
icon: <Network />,
onClick: handleOpenGraph,
}
: null;

const baseOperations: DetailOperation[] = [
{
key: "edit",
Expand Down Expand Up @@ -221,7 +227,7 @@ const KnowledgeBaseDetailPage: React.FC = () => {
},
];

const operations: DetailOperation[] = [graphOperation, ...baseOperations].filter(Boolean) as DetailOperation[];
const operations: DetailOperation[] = baseOperations;

const fileOps = [
{
Expand All @@ -242,7 +248,17 @@ const KnowledgeBaseDetailPage: React.FC = () => {
ellipsis: true,
fixed: "left" as const,
render: (_: unknown, file: KBFile) => (
<a onClick={() => navigate(`/data/knowledge-base/file-detail/${file.id}?knowledgeBaseId=${knowledgeBase?.id || ''}&fileName=${encodeURIComponent(file.name || file.fileName || '')}`)}>
<a
onClick={() => {
if (knowledgeBase?.type === KBType.GRAPH) {
handleOpenGraph();
return;
}
navigate(
`/data/knowledge-base/file-detail/${file.id}?knowledgeBaseId=${knowledgeBase?.id || ""}&fileName=${encodeURIComponent(file.name || file.fileName || "")}`
);
}}
>
{file.name}
</a>
)
Expand Down Expand Up @@ -478,16 +494,146 @@ const KnowledgeBaseDetailPage: React.FC = () => {
<Spin className="mt-8" />
) : recallResults.length === 0 ? (
<Empty description={t("knowledgeBase.detail.recallTest.noResult")} />
) : knowledgeBase?.type === KBType.GRAPH ? (
<div className="w-full">
{(() => {
const item = recallResults[0];
if (!item) return null;
return (
<div className="border border-gray-200 rounded-lg bg-white overflow-hidden">
<div className="flex items-center justify-between px-5 py-3 bg-gradient-to-r from-slate-50 to-gray-50 border-b border-gray-200">
<div className="text-xs text-gray-500 font-mono break-all">
ID: {item.id ?? "-"}
</div>
</div>
<div className="p-5">
<div className="prose prose-slate prose-sm max-w-none
prose-headings:text-slate-800 prose-headings:font-semibold
prose-p:text-gray-700 prose-p:leading-relaxed prose-p:m-0
prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline
prose-strong:text-slate-800 prose-em:text-slate-600
prose-li:text-gray-700
prose-code:before:content-none prose-code:after:content-none
prose-code:bg-slate-100 prose-code:text-rose-600 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-medium prose-code:whitespace-nowrap
prose-pre:bg-slate-900 prose-pre:shadow-lg
prose-blockquote:border-l-blue-400 prose-blockquote:bg-slate-50 prose-blockquote:py-1 prose-blockquote:not-italic
prose-table:border-collapse prose-th:bg-slate-100 prose-th:border prose-th:border-slate-300 prose-th:px-3 prose-th:py-2
prose-td:border prose-td:border-slate-200 prose-td:px-3 prose-td:py-2
prose-img:rounded-lg prose-img:shadow-md
prose-hr:border-slate-200">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
const codeString = String(children).replace(/\n$/, '');
const shouldRenderInline = inline ?? (!match && !codeString.includes("\n"));

if (shouldRenderInline) {
return (
<code className="text-slate-700 bg-slate-100 px-1.5 py-0.5 rounded text-sm font-mono inline" {...props}>
{children}
</code>
);
}

// 有指定语言的代码块才高亮
if (match) {
return (
<SyntaxHighlighter
{...props}
style={vscDarkPlus}
language={match[1]}
PreTag="div"
customStyle={{
borderRadius: '0.5rem',
padding: '1rem',
fontSize: '0.8rem',
margin: '0.5rem 0',
overflow: 'auto',
maxWidth: '100%'
}}
>
{codeString}
</SyntaxHighlighter>
);
}

// 无语言标记的代码块,以普通文本显示(不高亮)
return (
<pre className="bg-transparent text-slate-700 p-0 overflow-x-auto text-sm whitespace-pre font-sans leading-relaxed">
{codeString}
</pre>
);
},
p: ({ children }) => (
<p className="text-gray-700 leading-relaxed m-0 inline-block !whitespace-nowrap">
{children}
</p>
),
ul: ({ children }) => (
<ul className="my-2 pl-5 list-disc overflow-x-auto !whitespace-nowrap">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="my-2 pl-5 list-decimal overflow-x-auto !whitespace-nowrap">
{children}
</ol>
),
li: ({ children }) => (
<li className="!whitespace-nowrap">
{children}
</li>
),
br: () => <span> </span>,
a: ({ href, children }) => (
<a
href={href}
className="text-blue-600 hover:text-blue-800 hover:underline transition-colors"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
table: ({ children }) => (
<div className="overflow-x-auto my-4 rounded border border-slate-200">
<table className="min-w-full">{children}</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-slate-50">{children}</thead>
),
th: ({ children }) => (
<th className="px-4 py-2 text-left text-sm font-semibold text-slate-700 border-b border-slate-200">{children}</th>
),
td: ({ children }) => (
<td className="px-4 py-2 text-sm text-slate-600 border-b border-slate-100">{children}</td>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-blue-400 bg-slate-50 pl-4 py-2 my-4 text-slate-600 italic rounded-r">{children}</blockquote>
),
}}
>
{squashSoftLineBreaksOutsideFences(item.text ?? "")}
</ReactMarkdown>
</div>
</div>
</div>
);
})()}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{recallResults.map((item, idx) => (
<Card key={idx} title={`${t("knowledgeBase.detail.recallTest.scoreLabel")}${item.score?.toFixed(4) ?? "-"}`}
extra={<span style={{ fontSize: 12 }}>ID: {item.entity?.id ?? "-"}</span>}
extra={<span style={{ fontSize: 12 }}>ID: {item.id ?? "-"}</span>}
style={{ wordBreak: "break-all" }}
>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{item.entity?.text ?? ""}</div>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{item.text ?? ""}</div>
<div style={{ fontSize: 12, color: '#888' }}>
{t("knowledgeBase.detail.recallTest.metadataLabel")} <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{item.entity?.metadata}</pre>
{t("knowledgeBase.detail.recallTest.metadataLabel")} <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{JSON.stringify(item.metadata, null, 2)}</pre>
</div>
</Card>
))}
Expand Down
18 changes: 11 additions & 7 deletions frontend/src/pages/KnowledgeBase/knowledge-base.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { get, post, put, del } from "@/utils/request";
import type { UnifiedSearchResult } from "./knowledge-base.model";

// 获取知识库列表
export function queryKnowledgeBasesUsingPost(params: any) {
Expand Down Expand Up @@ -59,21 +60,17 @@ export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: obj
return (del as unknown as (url: string, data?: object | null) => Promise<unknown>)(`/api/knowledge-base/${baseId}/files`, data ?? null);
}

export function fetchKnowledgeGraph(data: { knowledge_base_id: string; query: string }) {
return post("/api/rag/query", data);
}

// 检索知识库内容
// 检索知识库内容(统一检索接口)
export function retrieveKnowledgeBaseContent(data: {
query: string;
topK?: number;
threshold?: number;
knowledgeBaseIds: string[];
}) {
}): Promise<UnifiedSearchResult[]> {
return post("/api/knowledge-base/retrieve", data);
}

// 新增:获取知识库文件详情(分页的切片数据)
// 获取知识库文件详情(分页的切片数据)
export function queryKnowledgeBaseFileDetailUsingGet(
knowledgeBaseId: string,
ragFileId: string,
Expand All @@ -83,3 +80,10 @@ export function queryKnowledgeBaseFileDetailUsingGet(
const size = params.size ?? 20;
return get(`/api/knowledge-base/${knowledgeBaseId}/files/${ragFileId}?page=${page}&page_size=${size}`);
}

export function queryKnowledgeBase(data: {
knowledge_base_id: string;
query: string;
}) {
return post("/api/knowledge-base/query", data);
}
10 changes: 10 additions & 0 deletions frontend/src/pages/KnowledgeBase/knowledge-base.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ export enum KBType {
GRAPH = "GRAPH",
}

export interface UnifiedSearchResult {
id: string;
text: string;
score: number;
metadata: Record<string, any>;
resultType: "vector" | "graph";
knowledgeBaseId: string;
knowledgeBaseName: string;
}

export interface KnowledgeBaseItem {
id: string;
name: string;
Expand Down
6 changes: 6 additions & 0 deletions runtime/datamate-python/app/core/exception/codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ def __init__(self):
RAG_EMBEDDING_FAILED: Final = ErrorCode(
"rag.0013", "Embedding generation failed", 500
)
RAG_UNSUPPORTED_TYPE: Final = ErrorCode(
"rag.0014", "Unsupported RAG type", 400
)
RAG_INVALID_REQUEST: Final = ErrorCode(
"rag.0015", "Invalid request", 400
)

# ========== 配比模块 ==========
RATIO_TASK_NOT_FOUND: Final = ErrorCode("ratio.0001", "Ratio task not found", 404)
Expand Down
3 changes: 0 additions & 3 deletions runtime/datamate-python/app/module/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from .generation.interface import router as generation_router
from .evaluation.interface import router as evaluation_router
from .collection.interface import router as collection_route
from .rag.interface.rag_interface import router as rag_router
from .operator.interface import operator_router
from .operator.interface import category_router
from .cleaning.interface import router as cleaning_router
Expand All @@ -22,11 +21,9 @@
router.include_router(generation_router)
router.include_router(evaluation_router)
router.include_router(collection_route)
router.include_router(rag_router)
router.include_router(operator_router)
router.include_router(category_router)
router.include_router(cleaning_router)

router.include_router(knowledge_base_router)

__all__ = ["router"]
2 changes: 0 additions & 2 deletions runtime/datamate-python/app/module/rag/interface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
集中导出所有 API 路由
"""
from .knowledge_base import router as knowledge_base_router
from .rag_interface import router as graph_rag_router

__all__ = [
"knowledge_base_router",
"graph_rag_router",
]
Loading
Loading