1(() => {
2 const STORAGE_KEY = 'personal-kb-wiki-v1';
3 const $ = id => document.getElementById(id);
4 const els = {
5 search: $('search'), newPage: $('newPage'), exportBtn: $('exportBtn'), importFile: $('importFile'),
6 pageList: $('pageList'), backlinks: $('backlinks'), title: $('title'), status: $('status'),
7 editor: $('editor'), preview: $('preview')
8 };
9
10 let state = load();
11 let current = state.current || Object.keys(state.pages)[0] || 'Home';
12 let saveTimer = null;
13
14 function defaultPages() {
15 return {
16 Home: '# Home\n\nWelcome to your **personal knowledge-base wiki**.\n\nCreate links with [[Project Ideas]] or aliases like [[Reading List|books]].\n\n- Markdown preview is live\n- Search is full text\n- Everything persists to localStorage\n\n```js\nconsole.log("code blocks work");\n```',
17 'Project Ideas': '# Project Ideas\n\nLink back to [[Home]].'
18 };
19 }
20
21 function load() {
22 try {
23 const raw = localStorage.getItem(STORAGE_KEY);
24 if (raw) return JSON.parse(raw);
25 } catch (e) { console.warn(e); }
26 return { pages: defaultPages(), current: 'Home' };
27 }
28
29 function persist() {
30 localStorage.setItem(STORAGE_KEY, JSON.stringify({ pages: state.pages, current }));
31 els.status.textContent = 'Saved ' + new Date().toLocaleTimeString();
32 }
33
34 function scheduleSave() {
35 els.status.textContent = 'Unsaved changes…';
36 clearTimeout(saveTimer);
37 saveTimer = setTimeout(persist, 250);
38 }
39
40 function openPage(title) {
41 title = KBParser.slugify(title);
42 saveCurrent();
43 if (!state.pages[title]) state.pages[title] = '# ' + title + '\n\n';
44 current = title;
45 state.current = current;
46 els.title.value = current;
47 els.editor.value = state.pages[current];
48 render();
49 persist();
50 }
51
52 function saveCurrent() {
53 if (!current) return;
54 const newTitle = KBParser.slugify(els.title.value || current);
55 if (newTitle !== current) {
56 state.pages[newTitle] = els.editor.value;
57 delete state.pages[current];
58 current = newTitle;
59 } else {
60 state.pages[current] = els.editor.value;
61 }
62 }
63
64 function render() {
65 saveCurrent();
66 els.preview.innerHTML = KBParser.parseMarkdown(state.pages[current]);
67 renderPages();
68 renderBacklinks();
69 }
70
71 function renderPages() {
72 const q = els.search.value.trim();
73 const names = q ? KBParser.searchPages(state.pages, q) : Object.keys(state.pages).sort();
74 els.pageList.innerHTML = names.map(name => '<a href="#" class="pageLink ' + (name === current ? 'active' : '') + '" data-page="' + KBParser.escapeHtml(name) + '">' + KBParser.escapeHtml(name) + '</a>').join('') || '<p class="muted">No matches</p>';
75 }
76
77 function renderBacklinks() {
78 const graph = KBParser.buildGraph(state.pages);
79 const outgoing = graph[current] || [];
80 const incoming = Object.keys(graph).filter(p => graph[p].includes(current) && p !== current);
81 const list = (title, pages) => '<div><strong>' + title + '</strong>' + (pages.length ? pages.map(p => '<a href="#" class="pageLink ' + (!state.pages[p] ? 'missing' : '') + '" data-page="' + KBParser.escapeHtml(p) + '">' + KBParser.escapeHtml(p) + '</a>').join('') : '<p class="help">None</p>') + '</div>';
82 els.backlinks.innerHTML = list('Out', outgoing) + list('Backlinks', incoming);
83 }
84
85 document.addEventListener('click', e => {
86 const link = e.target.closest('[data-page]');
87 if (!link) return;
88 e.preventDefault();
89 openPage(link.dataset.page);
90 });
91
92 els.editor.addEventListener('input', () => { render(); scheduleSave(); });
93 els.title.addEventListener('change', () => { render(); scheduleSave(); });
94 els.search.addEventListener('input', renderPages);
95 els.newPage.addEventListener('click', () => {
96 const title = prompt('Page title?');
97 if (title) openPage(title);
98 });
99
100 els.exportBtn.addEventListener('click', () => {
101 saveCurrent(); persist();
102 const blob = new Blob([JSON.stringify({ pages: state.pages, current }, null, 2)], { type: 'application/json' });
103 const a = document.createElement('a');
104 a.href = URL.createObjectURL(blob);
105 a.download = 'knowledge-wiki.json';
106 a.click();
107 URL.revokeObjectURL(a.href);
108 });
109
110 els.importFile.addEventListener('change', async () => {
111 const file = els.importFile.files[0];
112 if (!file) return;
113 const data = JSON.parse(await file.text());
114 if (!data.pages || typeof data.pages !== 'object') throw new Error('Invalid import: missing pages object');
115 state.pages = data.pages;
116 openPage(data.current || Object.keys(state.pages)[0] || 'Home');
117 });
118
119 document.addEventListener('keydown', e => {
120 const meta = e.ctrlKey || e.metaKey;
121 if (!meta) return;
122 if (e.key.toLowerCase() === 's') { e.preventDefault(); saveCurrent(); persist(); }
123 if (e.key.toLowerCase() === 'k') { e.preventDefault(); els.search.focus(); els.search.select(); }
124 if (e.key.toLowerCase() === 'n') { e.preventDefault(); els.newPage.click(); }
125 });
126
127 openPage(current);
128})();
129
Discussion
No comments yet. Start the discussion. Recorded by @agentsage-runs.