amerc/src/DocsApp.jsx
artheru b055663372 amerc suite: 2D pixel tavern + zero-dep auth/admin/docs/pm backend
- Scene2D pixel tavern (replaces three.js 3D scene)
- amerc-api: node:http+node:sqlite+node:crypto, auth/admin/docs/files/boards
- docs.amerc.ai + pm.amerc.ai (whiteboard mindmap + netdisk) apps
- agent API keys for fleet read/write

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 06:03:40 +08:00

87 lines
4.5 KiB
JavaScript

import React, { useCallback, useEffect, useState } from 'react';
import { marked } from 'marked';
import ConsoleShell from './ConsoleShell.jsx';
import { api } from './api.js';
marked.setOptions({ breaks: true });
function DocsWorkspace() {
const [docs, setDocs] = useState([]);
const [sel, setSel] = useState(null);
const [doc, setDoc] = useState(null);
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState({ title: '', folder: '', body: '' });
const [err, setErr] = useState('');
const [q, setQ] = useState('');
const loadList = useCallback(async () => {
try { const d = await api('/docs'); setDocs(d.docs || []); } catch (e) { setErr(e.message); }
}, []);
useEffect(() => { loadList(); }, [loadList]);
useEffect(() => {
if (sel == null) { setDoc(null); return; }
api(`/docs/${sel}`).then((d) => { setDoc(d.doc); setDraft({ title: d.doc.title, folder: d.doc.folder, body: d.doc.body }); setEditing(false); }).catch((e) => setErr(e.message));
}, [sel]);
const create = async () => {
const title = window.prompt('Document title', 'New document'); if (!title) return;
try { const r = await api('/docs', { method: 'POST', body: { title, folder: '', body: `# ${title}\n\n` } }); await loadList(); setSel(r.id); setEditing(true); } catch (e) { setErr(e.message); }
};
const save = async () => { try { await api(`/docs/${sel}`, { method: 'PATCH', body: draft }); await loadList(); setDoc({ ...doc, ...draft }); setEditing(false); } catch (e) { setErr(e.message); } };
const del = async () => { if (!window.confirm('Delete document?')) return; try { await api(`/docs/${sel}`, { method: 'DELETE' }); setSel(null); loadList(); } catch (e) { setErr(e.message); } };
const filtered = docs.filter((d) => (d.title + ' ' + (d.folder || '')).toLowerCase().includes(q.toLowerCase()));
const folders = {};
filtered.forEach((d) => { (folders[d.folder || ''] = folders[d.folder || ''] || []).push(d); });
return (
<div className="docs">
<aside className="docs-side">
<div className="docs-side-top">
<input placeholder="search docs…" value={q} onChange={(e) => setQ(e.target.value)} />
<button className="cs-btn cs-primary" onClick={create}>+ New</button>
</div>
<div className="docs-tree">
{Object.keys(folders).sort().map((f) => (
<div key={f} className="docs-folder">
<div className="docs-folder-name">{f || '📁 root'}</div>
{folders[f].map((d) => (
<button key={d.id} className={`docs-item${sel === d.id ? ' on' : ''}`} onClick={() => setSel(d.id)}>{d.title}</button>
))}
</div>
))}
{!filtered.length && <p className="cs-hint">No documents yet. Create one humans and agents (via API key) can both read & write here.</p>}
</div>
</aside>
<section className="docs-main">
{err && <p className="cs-err">{err}</p>}
{!doc && <div className="docs-empty">Select or create a document.</div>}
{doc && (
<>
<div className="docs-head">
{editing
? <input className="docs-title-input" value={draft.title} onChange={(e) => setDraft({ ...draft, title: e.target.value })} />
: <h2>{doc.title}</h2>}
<div className="docs-actions">
{editing && <input className="docs-folder-input" placeholder="folder" value={draft.folder} onChange={(e) => setDraft({ ...draft, folder: e.target.value })} />}
{!editing && <button className="cs-btn" onClick={() => setEditing(true)}>Edit</button>}
{editing && <button className="cs-btn cs-primary" onClick={save}>Save</button>}
{editing && <button className="cs-btn" onClick={() => { setEditing(false); setDraft({ title: doc.title, folder: doc.folder, body: doc.body }); }}>Cancel</button>}
<button className="cs-btn" onClick={del}>Delete</button>
</div>
</div>
<p className="docs-byline">updated by {doc.updated_by || '—'}</p>
{editing
? <textarea className="docs-editor" value={draft.body} onChange={(e) => setDraft({ ...draft, body: e.target.value })} />
: <article className="docs-render" dangerouslySetInnerHTML={{ __html: marked.parse(doc.body || '') }} />}
</>
)}
</section>
</div>
);
}
export default function DocsApp() {
return <ConsoleShell title="docs" accent="#27d8ff" current="docs">{() => <DocsWorkspace />}</ConsoleShell>;
}