Changeset 0.37.7 (#219)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
182
src/js/sw/surfaces/settings/knowledge.js
Normal file
182
src/js/sw/surfaces/settings/knowledge.js
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Settings > Knowledge — personal knowledge base management
|
||||
*/
|
||||
const { html } = window;
|
||||
const { useState, useEffect, useCallback } = hooks;
|
||||
|
||||
function fmtDate(d) {
|
||||
if (!d) return '\u2014';
|
||||
return new Date(d).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
|
||||
}
|
||||
|
||||
function formatBytes(b) {
|
||||
if (!b) return '0 B';
|
||||
if (b < 1024) return b + ' B';
|
||||
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
|
||||
return (b / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
export default function KnowledgeSection() {
|
||||
const [kbs, setKbs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detail, setDetail] = useState(null);
|
||||
const [docs, setDocs] = useState([]);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const data = await sw.api.knowledge.list();
|
||||
setKbs(Array.isArray(data) ? data : data.data || []);
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
async function openDetail(kb) {
|
||||
setDetail(kb);
|
||||
try {
|
||||
const d = await sw.api.knowledge.documents(kb.id);
|
||||
setDocs(Array.isArray(d) ? d : d.data || []);
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function createKb(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const name = form.name.value.trim();
|
||||
if (!name) { sw.toast('Name is required', 'error'); return; }
|
||||
try {
|
||||
await sw.api.knowledge.create({
|
||||
name,
|
||||
description: form.description.value.trim(),
|
||||
});
|
||||
sw.toast('Knowledge base created', 'success');
|
||||
form.reset();
|
||||
setShowCreate(false);
|
||||
load();
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function deleteKb(id) {
|
||||
const ok = await sw.confirm('Delete this knowledge base and all its documents?', true);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await sw.api.knowledge.del(id);
|
||||
sw.toast('Knowledge base deleted', 'success');
|
||||
setDetail(null);
|
||||
load();
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function uploadDoc() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.txt,.md,.csv,.html,.pdf';
|
||||
input.onchange = async () => {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
await sw.api.knowledge.upload(detail.id, file);
|
||||
sw.toast('Document uploaded', 'success');
|
||||
const d = await sw.api.knowledge.documents(detail.id);
|
||||
setDocs(Array.isArray(d) ? d : d.data || []);
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
async function deleteDoc(docId) {
|
||||
const ok = await sw.confirm('Delete this document?', true);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await sw.api.knowledge.delDoc(detail.id, docId);
|
||||
sw.toast('Document deleted', 'success');
|
||||
setDocs(prev => prev.filter(d => d.id !== docId));
|
||||
} catch (e) { sw.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
function statusBadge(status) {
|
||||
const cls = status === 'ready' ? 'badge-active' : status === 'error' ? 'badge-inactive' : 'badge';
|
||||
return html`<span class="badge ${cls}">${status}</span>`;
|
||||
}
|
||||
|
||||
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
|
||||
|
||||
// Detail view
|
||||
if (detail) return html`
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<button class="btn-small" onClick=${() => { setDetail(null); setDocs([]); }}>\u2190 Back</button>
|
||||
<h4 style="margin:0;">${detail.name}</h4>
|
||||
${statusBadge(detail.status || 'active')}
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn-small" onClick=${uploadDoc}>Upload Document</button>
|
||||
<button class="btn-small btn-danger" onClick=${() => deleteKb(detail.id)}>Delete KB</button>
|
||||
</div>
|
||||
|
||||
${detail.description && html`
|
||||
<p class="text-muted" style="font-size:13px;margin:0 0 8px;">${detail.description}</p>
|
||||
`}
|
||||
<p class="text-muted" style="font-size:12px;margin-bottom:12px;">
|
||||
${detail.document_count || 0} docs \u2022 Created ${fmtDate(detail.created_at)}
|
||||
</p>
|
||||
|
||||
<div class="admin-list">
|
||||
${docs.length === 0 && html`<div class="empty-hint">No documents yet. Upload one to get started.</div>`}
|
||||
${docs.map(d => html`
|
||||
<div class="admin-surface-row" key=${d.id}>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<strong>${d.filename}</strong>
|
||||
<span class="text-muted" style="margin-left:8px;font-size:11px;">${formatBytes(d.size_bytes)}</span>
|
||||
${' '}${statusBadge(d.status || 'pending')}
|
||||
</div>
|
||||
<span class="text-muted" style="font-size:11px;">${fmtDate(d.created_at)}</span>
|
||||
<button class="btn-small btn-danger" onClick=${() => deleteDoc(d.id)}>Delete</button>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// List view
|
||||
return html`
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span class="text-muted" style="font-size:12px;">${kbs.length} knowledge base${kbs.length !== 1 ? 's' : ''}</span>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn-md btn-primary" onClick=${() => setShowCreate(!showCreate)}>
|
||||
${showCreate ? 'Cancel' : '+ Create'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${showCreate && html`
|
||||
<form class="settings-section" style="margin-bottom:16px;" onSubmit=${createKb}>
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input name="name" placeholder="e.g. Product Documentation" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<input name="description" placeholder="Optional description" />
|
||||
</div>
|
||||
<button type="submit" class="btn-small btn-primary">Create Knowledge Base</button>
|
||||
</form>
|
||||
`}
|
||||
|
||||
<div class="admin-list">
|
||||
${kbs.length === 0 && html`<div class="empty-hint">No knowledge bases. Create one to get started.</div>`}
|
||||
${kbs.map(k => html`
|
||||
<div class="admin-surface-row" key=${k.id} style="cursor:pointer;" onClick=${() => openDetail(k)}>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<strong>${k.name}</strong>
|
||||
<span class="text-muted" style="margin-left:8px;font-size:12px;">${k.document_count || 0} docs</span>
|
||||
</div>
|
||||
<span class="text-muted" style="font-size:11px;">${fmtDate(k.created_at)}</span>
|
||||
${statusBadge(k.status || 'active')}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user