feat: add markdown word editor for knowledge workspace
This commit is contained in:
1 parent
68c4896088
commit
64a5305bbf
8 files changed
+297
-28
No files matched your search
@@ -317,6 +317,12 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
icon: BookOpen,
|
||||
path: "/page/workspace/knowledge",
|
||||
},
|
||||
{
|
||||
id: "resource-markdown-docx-editor",
|
||||
label: "Markdown 转 Word",
|
||||
icon: FileText,
|
||||
path: "/page/workspace/knowledge/markdown-editor",
|
||||
},
|
||||
{
|
||||
id: "resource-knowledge-base",
|
||||
label: "知识库资源管理",
|
||||
|
||||
@@ -37,7 +37,7 @@ function AdminWeKnoraSettingsButton() {
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={() => setOpen(true)}>
|
||||
<Settings2 className="mr-2 size-4" />
|
||||
配置 WeKnora
|
||||
配置知识服务
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { languages } from "@codemirror/language-data";
|
||||
import { basicLightInit } from "@uiw/codemirror-theme-basic";
|
||||
import { monokaiInit } from "@uiw/codemirror-theme-monokai";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { Download, FileUp, Loader2, RefreshCw } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ReportMarkdownView } from "@/components/wiki/ReportMarkdownView";
|
||||
import { downloadMarkdownDocxFromBackend } from "@/core/api/markdown-docx-export";
|
||||
|
||||
const SAMPLE_MARKDOWN = `# Markdown 文档
|
||||
|
||||
在左侧编辑 Markdown,右侧会实时预览。
|
||||
|
||||
## 支持内容
|
||||
|
||||
- 标题、列表、引用
|
||||
- 表格和代码块
|
||||
- 下载为 Word 文档
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| 上传 | 支持 .md / .markdown |
|
||||
| 导出 | 生成 .docx |
|
||||
`;
|
||||
|
||||
const editorDarkTheme = monokaiInit({
|
||||
settings: {
|
||||
background: "transparent",
|
||||
gutterBackground: "transparent",
|
||||
fontSize: "14px",
|
||||
},
|
||||
});
|
||||
|
||||
const editorLightTheme = basicLightInit({
|
||||
settings: {
|
||||
background: "transparent",
|
||||
fontSize: "14px",
|
||||
},
|
||||
});
|
||||
|
||||
function stripMarkdownExtension(name: string): string {
|
||||
return name.replace(/\.(markdown|md)$/i, "");
|
||||
}
|
||||
|
||||
function isMarkdownFilename(name: string): boolean {
|
||||
return /\.(markdown|md)$/i.test(name);
|
||||
}
|
||||
|
||||
export default function MarkdownDocxEditorPage() {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [title, setTitle] = useState("Markdown 文档");
|
||||
const [markdownText, setMarkdownText] = useState(SAMPLE_MARKDOWN);
|
||||
const [fileName, setFileName] = useState("markdown-document");
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const extensions = useMemo(
|
||||
() => [
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
codeLanguages: languages,
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleFile = async (file: File | undefined) => {
|
||||
if (!file) return;
|
||||
if (!isMarkdownFilename(file.name)) {
|
||||
toast.error("请上传 .md 或 .markdown 文件");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await file.text();
|
||||
setMarkdownText(text);
|
||||
const baseName = stripMarkdownExtension(file.name);
|
||||
setFileName(baseName || "markdown-document");
|
||||
setTitle(baseName || "Markdown 文档");
|
||||
toast.success("Markdown 文件已载入");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "读取文件失败");
|
||||
} finally {
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!markdownText.trim()) {
|
||||
toast.error("当前 Markdown 内容为空");
|
||||
return;
|
||||
}
|
||||
setExporting(true);
|
||||
try {
|
||||
await downloadMarkdownDocxFromBackend({
|
||||
title: title.trim() || fileName || "Markdown 文档",
|
||||
markdown: markdownText,
|
||||
});
|
||||
toast.success("Word 文档已生成");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "导出 Word 失败");
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-background">
|
||||
<header className="shrink-0 border-b px-6 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl font-semibold">Markdown 转 Word</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
上传 md 文件后在线编辑,确认内容后下载为 Word。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept=".md,.markdown,text/markdown,text/plain"
|
||||
className="hidden"
|
||||
onChange={(event) => void handleFile(event.target.files?.[0])}
|
||||
/>
|
||||
<Button variant="outline" onClick={() => inputRef.current?.click()}>
|
||||
<FileUp className="size-4" />
|
||||
上传 md
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setMarkdownText(SAMPLE_MARKDOWN);
|
||||
setTitle("Markdown 文档");
|
||||
setFileName("markdown-document");
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
重置
|
||||
</Button>
|
||||
<Button onClick={handleExport} disabled={exporting || !markdownText.trim()}>
|
||||
{exporting ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="size-4" />
|
||||
)}
|
||||
下载 Word
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 max-w-xl">
|
||||
<label htmlFor="markdown-docx-title" className="mb-1 block text-sm font-medium">
|
||||
文档标题
|
||||
</label>
|
||||
<Input
|
||||
id="markdown-docx-title"
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
placeholder="请输入导出的 Word 标题"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="grid min-h-0 flex-1 gap-0 lg:grid-cols-2">
|
||||
<section className="flex min-h-0 flex-col border-r">
|
||||
<div className="flex h-10 shrink-0 items-center border-b px-4 text-sm font-medium">
|
||||
Markdown 编辑
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<CodeMirror
|
||||
value={markdownText}
|
||||
onChange={setMarkdownText}
|
||||
theme={resolvedTheme === "dark" ? editorDarkTheme : editorLightTheme}
|
||||
extensions={extensions}
|
||||
basicSetup={{
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
highlightActiveLineGutter: false,
|
||||
lineNumbers: true,
|
||||
}}
|
||||
className="h-full overflow-auto font-mono [&_.cm-editor]:h-full [&_.cm-focused]:outline-none!"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="min-h-0 overflow-auto bg-muted/20 p-4">
|
||||
<ReportMarkdownView
|
||||
content={markdownText || "暂无内容"}
|
||||
title={title || fileName}
|
||||
badgeLabel="Markdown 预览"
|
||||
className="max-w-none"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { languages } from "@codemirror/language-data";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { basicLightInit } from "@uiw/codemirror-theme-basic";
|
||||
import { monokaiInit } from "@uiw/codemirror-theme-monokai";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import {
|
||||
BookOpen,
|
||||
Check,
|
||||
@@ -39,7 +44,6 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -55,6 +59,7 @@ import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
import { createMarkdownAnchorResolver } from "@/core/utils/markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
type PublicTask = {
|
||||
task_id: string;
|
||||
@@ -903,6 +908,58 @@ function PublicFullscreenPreviewContent({ preview }: { preview: PublicFullscreen
|
||||
return <pre className="whitespace-pre-wrap break-words text-sm">{preview.content || "无正文内容"}</pre>;
|
||||
}
|
||||
|
||||
const markdownEditorDarkTheme = monokaiInit({
|
||||
settings: {
|
||||
background: "transparent",
|
||||
gutterBackground: "transparent",
|
||||
fontSize: "14px",
|
||||
},
|
||||
});
|
||||
|
||||
const markdownEditorLightTheme = basicLightInit({
|
||||
settings: {
|
||||
background: "transparent",
|
||||
fontSize: "14px",
|
||||
},
|
||||
});
|
||||
|
||||
function PublicMarkdownEditor({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const extensions = useMemo(
|
||||
() => [
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
codeLanguages: languages,
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<CodeMirror
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
theme={resolvedTheme === "dark" ? markdownEditorDarkTheme : markdownEditorLightTheme}
|
||||
extensions={extensions}
|
||||
basicSetup={{
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
highlightActiveLineGutter: false,
|
||||
lineNumbers: true,
|
||||
}}
|
||||
className="h-full overflow-auto font-mono [&_.cm-editor]:h-full [&_.cm-focused]:outline-none!"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function publicFullscreenHref(taskId: string, runId: string, tab: ContentTab): string {
|
||||
const params = new URLSearchParams();
|
||||
params.set("tab", tab.kind);
|
||||
@@ -1320,12 +1377,7 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
||||
正在加载文件内容...
|
||||
</div>
|
||||
) : isEditing ? (
|
||||
<Textarea
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
className="min-h-0 flex-1 resize-none font-mono text-sm"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<PublicMarkdownEditor value={draft} onChange={setDraft} />
|
||||
) : (
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
{showEmptyHint ? (
|
||||
|
||||
@@ -40,6 +40,7 @@ import ScheduledTaskRunDetailPage from './ScheduledTaskRunDetailPage';
|
||||
import ScheduledTasksPage from './ScheduledTasksPage';
|
||||
import LlmWikiKnowledgePage from './LlmWikiKnowledgePage';
|
||||
import WeKnoraWikiManagementPage from '@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage';
|
||||
import MarkdownDocxEditorPage from './MarkdownDocxEditorPage';
|
||||
import LlmMetricsPage from './LlmMetricsPage';
|
||||
import CheckpointMigrationPage from './CheckpointMigrationPage';
|
||||
import MemoryPage from './MemoryPage';
|
||||
@@ -665,6 +666,14 @@ export default function WorkspaceRoutes({
|
||||
</WorkspaceLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="knowledge/markdown-editor"
|
||||
element={
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<MarkdownDocxEditorPage />
|
||||
</WorkspaceLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="knowledge/:noteId"
|
||||
element={
|
||||
|
||||
+4
-4
@@ -150,7 +150,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
})()
|
||||
.catch((error: unknown) => {
|
||||
if (cancelled) return;
|
||||
const message = error instanceof Error ? error.message : "创建 WeKnora 详情会话失败";
|
||||
const message = error instanceof Error ? error.message : "创建知识库详情会话失败";
|
||||
setFrameError(message);
|
||||
});
|
||||
|
||||
@@ -195,7 +195,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
<AlertCircle className="size-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-foreground">无法打开 WeKnora 详情</div>
|
||||
<div className="text-base font-semibold text-foreground">无法打开知识库详情</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{frameError}</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => navigate(LIST_PATH)}>
|
||||
@@ -214,7 +214,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
<div>
|
||||
<div className="text-base font-semibold text-foreground">无法打开知识库详情</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
当前账号没有访问权限,或 WeKnora 资源已不存在。
|
||||
当前账号没有访问权限,或知识服务资源已不存在。
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => navigate(LIST_PATH)}>
|
||||
@@ -319,7 +319,7 @@ export function WeKnoraKnowledgeDetailPage() {
|
||||
<iframe
|
||||
key={`${frameSrc}:${frameVersion}`}
|
||||
ref={frameRef}
|
||||
title={`${details.data.name || "WeKnora"} 详情`}
|
||||
title={`${details.data.name || "知识库"} 详情`}
|
||||
src={frameSrc}
|
||||
className="h-full min-h-0 w-full flex-1 border-0 bg-background"
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-downloads allow-popups"
|
||||
|
||||
+15
-15
@@ -76,7 +76,7 @@ export function WeKnoraKnowledgeWorkspace() {
|
||||
const busy = actions.create.isPending || actions.remove.isPending || actions.publication.isPending;
|
||||
const openDetails = (item: LlmWikiKnowledgeBase) => {
|
||||
if (item.remote_status === "missing") {
|
||||
toast.error("该知识库已从 WeKnora 删除,只能清理当前记录");
|
||||
toast.error("该知识库已从知识服务删除,只能清理当前记录");
|
||||
return;
|
||||
}
|
||||
navigate(`${LIST_PATH}/${encodeURIComponent(item.id)}`);
|
||||
@@ -97,8 +97,8 @@ export function WeKnoraKnowledgeWorkspace() {
|
||||
|
||||
const removeKnowledgeBase = async (item: LlmWikiKnowledgeBase) => {
|
||||
const warning = item.remote_status === "missing"
|
||||
? `知识库“${item.name}”已不在 WeKnora 中。确定清理 cmzs 中的这条失效记录吗?`
|
||||
: `确定删除知识库“${item.name}”吗?WeKnora 中的文档和索引也会删除。`;
|
||||
? `知识库“${item.name}”已不在知识服务中。确定清理 cmzs 中的这条失效记录吗?`
|
||||
: `确定删除知识库“${item.name}”吗?知识服务中的文档和索引也会删除。`;
|
||||
if (!window.confirm(warning)) return;
|
||||
try {
|
||||
await actions.remove.mutateAsync(item.id);
|
||||
@@ -126,9 +126,9 @@ export function WeKnoraKnowledgeWorkspace() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="size-5 text-primary" />
|
||||
<h1 className="text-xl font-semibold">问答梳理</h1>
|
||||
<Badge variant="secondary">WeKnora</Badge>
|
||||
<Badge variant="secondary">知识服务</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">由 WeKnora 负责解析与检索,用户、权限和发布范围由 cmzs 管理。</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">由知识服务负责解析与检索,用户、权限和发布范围由 cmzs 管理。</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded-full border bg-background/80 p-1 shadow-sm backdrop-blur">
|
||||
{tab !== "settings" && (
|
||||
@@ -168,7 +168,7 @@ export function WeKnoraKnowledgeWorkspace() {
|
||||
|
||||
{searchResults.length > 0 && tab !== "settings" && (
|
||||
<Card className="mt-5">
|
||||
<CardHeader className="pb-3"><CardTitle className="text-base">检索结果</CardTitle><CardDescription>结果由 WeKnora 检索返回,问答中使用同一授权链路。</CardDescription></CardHeader>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-base">检索结果</CardTitle><CardDescription>结果由知识服务检索返回,问答中使用同一授权链路。</CardDescription></CardHeader>
|
||||
<CardContent className="space-y-3">{searchResults.map((result) => <div key={result.chunk_id} className="rounded-lg border p-3"><div className="flex items-center justify-between gap-3 text-sm font-medium"><span>{result.title || "未命名来源"}</span><Badge variant="outline">{result.knowledge_base_name}</Badge></div><p className="mt-2 line-clamp-3 whitespace-pre-wrap text-sm text-muted-foreground">{result.content}</p></div>)}</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -235,7 +235,7 @@ function KnowledgeGrid({
|
||||
<CardContent className="border-t bg-muted/20 px-6 py-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{item.remote_status === "missing" ? <span className="text-xs text-muted-foreground">WeKnora 资源不存在,可清理本地记录</span> : <Button size="sm" variant="outline" className="rounded-full" onClick={() => onOpen(item)}><FileText className="mr-2 size-4" />查看详情</Button>}
|
||||
{item.remote_status === "missing" ? <span className="text-xs text-muted-foreground">知识服务资源不存在,可清理本地记录</span> : <Button size="sm" variant="outline" className="rounded-full" onClick={() => onOpen(item)}><FileText className="mr-2 size-4" />查看详情</Button>}
|
||||
{item.remote_status !== "missing" && item.is_owner && !item.is_conversation_deposit && <Button size="sm" variant="secondary" className="rounded-full" disabled={busy} onClick={() => void onPublish(item)}>{item.publication_status === "published" ? "取消公开" : "发布公共库"}</Button>}
|
||||
</div>
|
||||
{item.can_write && !item.is_conversation_deposit && <Button size="icon" variant="ghost" className="size-8 shrink-0 rounded-full text-destructive hover:bg-destructive/10 hover:text-destructive" aria-label={item.remote_status === "missing" ? `清理失效记录 ${item.name}` : `删除知识库 ${item.name}`} title={item.remote_status === "missing" ? "清理失效记录" : "删除知识库"} disabled={busy} onClick={() => void onDelete(item)}><Trash2 className="size-4" /></Button>}
|
||||
@@ -267,7 +267,7 @@ function CreateKnowledgeDialog({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle>新建个人知识库</DialogTitle><DialogDescription>选择知识库模式;模型、切块和存储策略仍由 WeKnora 管理。</DialogDescription></DialogHeader>
|
||||
<DialogHeader><DialogTitle>新建个人知识库</DialogTitle><DialogDescription>选择知识库模式;模型、切块和存储策略仍由知识服务管理。</DialogDescription></DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="llmwiki-name">名称</label><Input id="llmwiki-name" value={name} onChange={(event) => setName(event.target.value)} maxLength={255} /></div>
|
||||
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="llmwiki-description">描述</label><Textarea id="llmwiki-description" value={description} onChange={(event) => setDescription(event.target.value)} rows={4} maxLength={2048} /></div>
|
||||
@@ -294,20 +294,20 @@ export function ConnectionSettings() {
|
||||
setWebUrl(settings.data.web_base_url || settings.data.file_web_base_url || "");
|
||||
}, [settings.data]);
|
||||
|
||||
const status = useMemo(() => settings.data?.effective_provider === "weknora" ? "WeKnora 模式" : "原 LLMWiki 模式", [settings.data]);
|
||||
const status = useMemo(() => settings.data?.effective_provider === "weknora" ? "知识服务模式" : "原 LLMWiki 模式", [settings.data]);
|
||||
if (settings.isLoading) return <div className="flex h-52 items-center justify-center"><Loader2 className="size-5 animate-spin" /></div>;
|
||||
if (settings.error) return <div className="rounded-lg border border-destructive/30 p-5 text-sm text-destructive">{messageOf(settings.error)}</div>;
|
||||
return (
|
||||
<Card className="max-w-3xl">
|
||||
<CardHeader><CardTitle className="flex items-center gap-2 text-base"><Settings2 className="size-5" />WeKnora 连接</CardTitle><CardDescription>这里只管理 cmzs 到 WeKnora 的连接地址。数据库、Redis、模型、存储和解析器仍在 WeKnora 自己的配置中维护。</CardDescription></CardHeader>
|
||||
<CardHeader><CardTitle className="flex items-center gap-2 text-base"><Settings2 className="size-5" />知识服务连接</CardTitle><CardDescription>这里只管理 cmzs 到知识服务的连接地址。数据库、Redis、模型、存储和解析器仍在服务端配置中维护。</CardDescription></CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<div className="flex flex-wrap gap-2 text-sm"><Badge>{status}</Badge><Badge variant="secondary">服务端代理鉴权</Badge>{settings.data?.override_enabled && <Badge variant="outline">页面配置覆盖中</Badge>}</div>
|
||||
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="weknora-api-url">WeKnora API 地址</label><Input id="weknora-api-url" value={apiUrl} onChange={(event) => setApiUrl(event.target.value)} placeholder="http://weknora-app:8080" /><p className="text-xs text-muted-foreground">非空启用 WeKnora;保存为空会切回原 LLMWiki。</p></div>
|
||||
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="weknora-web-url">WeKnora 管理地址(可选)</label><Input id="weknora-web-url" value={webUrl} onChange={(event) => setWebUrl(event.target.value)} placeholder="https://weknora-admin.example.com" /></div>
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-xs text-muted-foreground">配置文件地址:{settings.data?.file_api_base_url || "未配置"}。页面只配置地址;WeKnora iframe 免登录所需的管理员账号/密码只从后端 config.yaml 或环境变量读取,不在前端保存。</div>
|
||||
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="weknora-api-url">知识服务 API 地址</label><Input id="weknora-api-url" value={apiUrl} onChange={(event) => setApiUrl(event.target.value)} placeholder="http://knowledge-service:8080" /><p className="text-xs text-muted-foreground">非空启用知识服务;保存为空会切回原 LLMWiki。</p></div>
|
||||
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="weknora-web-url">知识服务管理地址(可选)</label><Input id="weknora-web-url" value={webUrl} onChange={(event) => setWebUrl(event.target.value)} placeholder="https://knowledge-admin.example.com" /></div>
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-xs text-muted-foreground">配置文件地址:{settings.data?.file_api_base_url || "未配置"}。页面只配置地址;免登录详情页所需的管理员账号/密码只从后端 config.yaml 或环境变量读取,不在前端保存。</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" disabled={!apiUrl.trim() || actions.test.isPending} onClick={async () => { try { await actions.test.mutateAsync(apiUrl.trim()); toast.success("WeKnora 连接正常"); } catch (error) { toast.error(messageOf(error)); } }}>{actions.test.isPending && <Loader2 className="mr-2 size-4 animate-spin" />}测试连接</Button>
|
||||
<Button disabled={actions.save.isPending} onClick={async () => { try { await actions.save.mutateAsync({ override_enabled: true, api_base_url: apiUrl.trim(), web_base_url: webUrl.trim() }); toast.success(apiUrl.trim() ? "已启用 WeKnora 模式" : "已切换到原 LLMWiki 模式"); } catch (error) { toast.error(messageOf(error)); } }}>{actions.save.isPending && <Loader2 className="mr-2 size-4 animate-spin" />}保存页面配置</Button>
|
||||
<Button variant="outline" disabled={!apiUrl.trim() || actions.test.isPending} onClick={async () => { try { await actions.test.mutateAsync(apiUrl.trim()); toast.success("知识服务连接正常"); } catch (error) { toast.error(messageOf(error)); } }}>{actions.test.isPending && <Loader2 className="mr-2 size-4 animate-spin" />}测试连接</Button>
|
||||
<Button disabled={actions.save.isPending} onClick={async () => { try { await actions.save.mutateAsync({ override_enabled: true, api_base_url: apiUrl.trim(), web_base_url: webUrl.trim() }); toast.success(apiUrl.trim() ? "已启用知识服务模式" : "已切换到原 LLMWiki 模式"); } catch (error) { toast.error(messageOf(error)); } }}>{actions.save.isPending && <Loader2 className="mr-2 size-4 animate-spin" />}保存页面配置</Button>
|
||||
<Button variant="secondary" disabled={actions.reset.isPending} onClick={async () => { try { await actions.reset.mutateAsync(); toast.success("已恢复 config.yaml 配置"); } catch (error) { toast.error(messageOf(error)); } }}>恢复配置文件</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
+1
-1
@@ -631,7 +631,7 @@ export function WeKnoraWikiManagementPage() {
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{creating ? "新建文章" : draft.title || selectedSlug}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{creating ? "创建后写入 WeKnora Wiki" : draft.slug}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{creating ? "创建后写入知识库 Wiki" : draft.slug}</div>
|
||||
{(draft.pageType || draft.status || draft.parentSlug || draft.aliasesText || draft.categoryPathText) && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{[draft.pageType, draft.status, draft.parentSlug, ...splitDirectory(draft.categoryPathText), ...splitTags(draft.aliasesText)]
|
||||
|
||||
Reference in new issue
Block a user