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

191 lines
6.1 KiB
TypeScript
Raw Normal View History

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 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]);
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>
<h1 className="kb-title"></h1>
</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'}
>
🌊 Agent
</button>
</div>
</header>
{/* 主体 */}
<div className="kb-body">
{/* 侧栏 */}
{sidebarOpen && (
<aside className="kb-sidebar">
<div className="kb-sidebar-actions">
<button className="kb-btn-small" onClick={() => createDoc('')}>
+
</button>
</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-icon">📖</div>
<p></p>
<p className="kb-empty-hint">+ </p>
</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>
);
}