- 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>
87 lines
4.5 KiB
JavaScript
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>;
|
|
}
|