hololake-system-architecture/product-source/guanghu-knowledge-base/src/App.tsx

243 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { api, DocTreeNode, DocContent } from './api';
import { DocTree } from './components/DocTree';
import { Editor } from './components/Editor';
import { SearchBar } from './components/SearchBar';
import { VersionHistory } from './components/VersionHistory';
import AgentChat from './components/AgentChat';
type View = 'editor' | 'history';
export default function App() {
const agentApiBase = window.location.protocol === 'file:' ? 'http://127.0.0.1:3890' : '';
const [tree, setTree] = useState<DocTreeNode[]>([]);
const [currentDoc, setCurrentDoc] = useState<DocContent | null>(null);
const [currentPath, setCurrentPath] = useState<string>('');
const [view, setView] = useState<View>('editor');
const [sidebarOpen, setSidebarOpen] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [agentPanelOpen, setAgentPanelOpen] = useState(true);
const [importing, setImporting] = useState(false);
const [importMessage, setImportMessage] = useState<string | null>(null);
const refreshTree = useCallback(async () => {
try {
const t = await api.getTree();
setTree(t);
} catch (err: any) {
setError(`加载文档树失败: ${err.message}`);
}
}, []);
const openDoc = useCallback(async (docPath: string) => {
setLoading(true);
setError(null);
try {
const doc = await api.getDoc(docPath);
setCurrentDoc(doc);
setCurrentPath(docPath);
setView('editor');
} catch (err: any) {
setError(`加载文档失败: ${err.message}`);
} finally {
setLoading(false);
}
}, []);
const saveDoc = useCallback(async (title: string, body: string) => {
if (!currentPath) return;
setLoading(true);
try {
const doc = await api.updateDoc(currentPath, title, body);
setCurrentDoc(doc);
await refreshTree();
} catch (err: any) {
setError(`保存失败: ${err.message}`);
} finally {
setLoading(false);
}
}, [currentPath, refreshTree]);
const createDoc = useCallback(async (parentPath: string) => {
const name = prompt('文档文件名(不含 .md');
if (!name) return;
const title = prompt('文档标题:') || name;
const docPath = parentPath ? `${parentPath}/${name}.md` : `${name}.md`;
try {
const doc = await api.createDoc(docPath, title, `# ${title}\n\n在这里开始写作...\n`);
setCurrentDoc(doc);
setCurrentPath(docPath);
await refreshTree();
} catch (err: any) {
setError(`创建失败: ${err.message}`);
}
}, [refreshTree]);
const deleteDoc = useCallback(async () => {
if (!currentPath) return;
if (!confirm(`确认删除 ${currentPath}`)) return;
try {
await api.deleteDoc(currentPath);
setCurrentDoc(null);
setCurrentPath('');
await refreshTree();
} catch (err: any) {
setError(`删除失败: ${err.message}`);
}
}, [currentPath, refreshTree]);
const importFolder = useCallback(async () => {
const bridge = (window as any).hololake?.knowledge;
if (!bridge?.importFolder) {
setError('本地文件夹导入只在 HoloLake 桌面 App 中提供');
return;
}
setImporting(true);
setError(null);
setImportMessage(null);
try {
const result = await bridge.importFolder();
if (result.cancelled) return;
if (!result.imported) {
setImportMessage(`没有找到可导入的文档;已跳过 ${result.skipped || 0} 个文件`);
return;
}
await refreshTree();
if (result.firstDocument) await openDoc(result.firstDocument);
const details = [
`已导入 ${result.imported} 篇文档`,
result.assets ? `${result.assets} 个图片资源` : '',
result.skipped ? `跳过 ${result.skipped} 个暂不支持的文件` : '',
result.failed?.length ? `${result.failed.length} 个文件失败` : '',
].filter(Boolean).join(' · ');
setImportMessage(details);
} catch (err: any) {
setError(`导入失败: ${err.message}`);
} finally {
setImporting(false);
}
}, [openDoc, refreshTree]);
useEffect(() => {
refreshTree();
}, [refreshTree]);
return (
<div className="kb-app">
{/* 顶栏 */}
<header className="kb-header">
<div className="kb-header-left">
<button
className="kb-btn-icon"
onClick={() => setSidebarOpen(!sidebarOpen)}
title={sidebarOpen ? '收起侧栏' : '展开侧栏'}
>
{sidebarOpen ? '◀' : '▶'}
</button>
<div className="kb-brand-mark" aria-hidden="true" />
<div>
<h1 className="kb-title">HoloLake</h1>
<div className="kb-subtitle"></div>
</div>
</div>
<div className="kb-header-center">
<SearchBar onSelect={openDoc} />
</div>
<div className="kb-header-right">
{currentDoc && (
<>
<button
className={`kb-btn-tab ${view === 'editor' ? 'active' : ''}`}
onClick={() => setView('editor')}
>
</button>
<button
className={`kb-btn-tab ${view === 'history' ? 'active' : ''}`}
onClick={() => setView('history')}
>
</button>
<button className="kb-btn-icon kb-btn-danger" onClick={deleteDoc} title="删除">
🗑
</button>
</>
)}
<button
className={`kb-btn-tab kb-btn-agent ${agentPanelOpen ? 'active' : ''}`}
onClick={() => setAgentPanelOpen(!agentPanelOpen)}
title={agentPanelOpen ? '收起 Agent' : '展开 Agent'}
>
HoloLake
</button>
</div>
</header>
{/* 主体 */}
<div className="kb-body">
{/* 侧栏 */}
{sidebarOpen && (
<aside className="kb-sidebar">
<div className="kb-workspace-label"></div>
<div className="kb-sidebar-actions">
<button className="kb-btn-import" onClick={importFolder} disabled={importing}>
<span className="kb-btn-import-icon"></span>
{importing ? '正在导入…' : '导入本地文件夹'}
</button>
<button className="kb-btn-new" onClick={() => createDoc('')}>
<span></span>
</button>
</div>
{importMessage && <div className="kb-import-message">{importMessage}</div>}
<div className="kb-tree-heading"></div>
<DocTree
nodes={tree}
currentPath={currentPath}
onSelect={openDoc}
onCreate={createDoc}
/>
</aside>
)}
{/* 内容区 */}
<main className="kb-content">
{error && (
<div className="kb-error">
{error}
<button onClick={() => setError(null)}></button>
</div>
)}
{loading && <div className="kb-loading">...</div>}
{!currentDoc && !loading && (
<div className="kb-empty">
<div className="kb-empty-card">
<div className="kb-empty-orbit"><span /></div>
<p className="kb-empty-eyebrow">HOLOLAKE KNOWLEDGE</p>
<h2></h2>
<p>HoloLake Git </p>
<button className="kb-empty-primary" onClick={importFolder} disabled={importing}>
{importing ? '正在导入…' : '选择本地文件夹'}
</button>
<div className="kb-empty-support"> MarkdownTXTCSVJSON YAML</div>
</div>
</div>
)}
{currentDoc && view === 'editor' && (
<Editor doc={currentDoc} onSave={saveDoc} />
)}
{currentDoc && view === 'history' && (
<VersionHistory docPath={currentPath} />
)}
</main>
{/* Agent 面板 */}
{agentPanelOpen && (
<aside className="kb-agent-panel">
<AgentChat apiBase={agentApiBase} onDocSelect={openDoc} />
</aside>
)}
</div>
</div>
);
}