From 89312a9ce4bafdfaa5bb668148efa56e66ff2d2d Mon Sep 17 00:00:00 2001 From: dtoro Date: Mon, 27 Jul 2026 22:50:54 +0200 Subject: [PATCH] feat(web): redesign Knowledge as an editable wiki Replaces the read-only stats dashboard with a three-pane wiki: a navigator tree (group by folder/type/tag/entity), a reader/editor with bare-slug auto-linking and revision history + diff, and a context rail for backlinks and related notes. Adds a Cleanup mode for the drift tools (duplicates, tag manager, orphans, trash) and a Cmd+K quick-open. Also: - Adds a real landing view (hero count, KPI row, Nomos-share meter, recently-updated, busiest tags) in place of the old "Select a note" empty state, and extends the design pass across the tree/reader/rail (kind icons instead of repeated text badges, accent-bar selection, constrained prose measure). - Guards every note-selection path behind a confirm when there's an unsaved edit in progress, so switching notes can no longer silently discard a draft. - Extracts the markdown-rendering CSS duplicated across ChatThread, EntityDetailContent, and the new WikiReader into a shared .markdown-body class in app.css, with ChatThread keeping only its decorative deltas. Co-Authored-By: Claude Sonnet 5 --- web/src/app.css | 89 +++ web/src/lib/api.ts | 255 ++++++++ web/src/lib/components/ChatThread.svelte | 83 +-- .../lib/components/EntityDetailContent.svelte | 70 +-- .../components/knowledge/WikiCleanup.svelte | 570 ++++++++++++++++++ .../knowledge/WikiContextRail.svelte | 124 ++++ .../components/knowledge/WikiNewDialog.svelte | 139 +++++ .../components/knowledge/WikiOverview.svelte | 201 ++++++ .../components/knowledge/WikiQuickOpen.svelte | 119 ++++ .../components/knowledge/WikiReader.svelte | 445 ++++++++++++++ .../lib/components/knowledge/WikiTree.svelte | 198 ++++++ web/src/lib/components/knowledge/kinds.ts | 50 ++ web/src/lib/components/knowledge/wikiText.ts | 197 ++++++ web/src/pages/Knowledge.svelte | 354 ++++++----- 14 files changed, 2626 insertions(+), 268 deletions(-) create mode 100644 web/src/lib/components/knowledge/WikiCleanup.svelte create mode 100644 web/src/lib/components/knowledge/WikiContextRail.svelte create mode 100644 web/src/lib/components/knowledge/WikiNewDialog.svelte create mode 100644 web/src/lib/components/knowledge/WikiOverview.svelte create mode 100644 web/src/lib/components/knowledge/WikiQuickOpen.svelte create mode 100644 web/src/lib/components/knowledge/WikiReader.svelte create mode 100644 web/src/lib/components/knowledge/WikiTree.svelte create mode 100644 web/src/lib/components/knowledge/kinds.ts create mode 100644 web/src/lib/components/knowledge/wikiText.ts diff --git a/web/src/app.css b/web/src/app.css index 48e26ed..b11dd96 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -333,3 +333,92 @@ a:hover { border-left: 1px solid var(--border); cursor: col-resize; } + +/* Base markdown rendering — used by every {@html marked.parse(...)} output + (EntityDetailContent, the Knowledge wiki's WikiReader, and as the + foundation ChatThread's fuller "Art Nouveau" chat styling builds on top + of). Global rather than a per-component diff --git a/web/src/lib/components/knowledge/WikiCleanup.svelte b/web/src/lib/components/knowledge/WikiCleanup.svelte new file mode 100644 index 0000000..90287f9 --- /dev/null +++ b/web/src/lib/components/knowledge/WikiCleanup.svelte @@ -0,0 +1,570 @@ + + +
+ + + activate('duplicates')}> + Duplicates + + activate('tags')}> + Tags + + activate('orphans')}> + Orphans + + activate('trash')}> + Trash + + + + +

+ Notes with near-identical titles, grouped for review — not a verdict. Pick a target and the + sources to fold into it; sources are soft-deleted afterward and stay recoverable from Trash. +

+ {#if mergeError} +

+ {mergeError} +

+ {/if} + {#if duplicatesError} +

+ {duplicatesError} + +

+ {/if} + {#if clusters === null} +
+ {:else if clusters.length === 0} +

No likely duplicates found.

+ {:else} +
+ {#each clusters as c (c.members[0].slug)} + {@const key = c.members[0].slug} + {@const sourceCount = mergeSources[key]?.size ?? 0} +
+
+
+ {c.members.length} similar notes + + + {(c.top_similarity * 100).toFixed(0)}% +
+ +
+ +

+ + keep as target + + + fold into it + +

+
+ {#each c.members as m (m.slug)} + {@const isTarget = mergeTarget[key] === m.slug} + {@const Icon = kindMeta(m.kind).icon} + + {/each} +
+
+ {/each} +
+ {/if} +
+ + + {#if tagsError} +

+ {tagsError} + +

+ {/if} + {#if tags === null} +
+ {:else} + {@const maxUses = Math.max(1, ...tags.map((t) => t.uses))} + + + + + + + + + + + {#each tags as t (t.tag)} + + + + + + + + + {/each} + +
TagUsesVariants
+ {#if renaming === t.tag} +
+ + +
+ {:else} + + {/if} +
{t.uses} + + + + + + {#if t.split} + {t.variants.join(', ')} + {:else} + + {/if} + + {#if t.split} + + {/if} +
+ {/if} +
+ + +

+ Notes untagged, unlinked to any entity, or untouched for 90+ days — invisible to most + navigation paths and easy to lose track of. + {#if orphanCounts.untagged || orphanCounts.unlinked || orphanCounts.stale} + ({orphanCounts.untagged ?? 0} untagged · {orphanCounts.unlinked ?? 0} unlinked · {orphanCounts.stale ?? + 0} stale) + {/if} +

+ {#if orphansError} +

+ {orphansError} + +

+ {/if} + {#if orphans === null} +
+ {:else if orphans.length === 0} +

Nothing orphaned.

+ {:else} +
+ {#each orphans as o (o.slug)} + + {/each} +
+ {/if} +
+ + + {#if trashError} +

+ {trashError} + +

+ {/if} + {#if trash === null} +
+ {:else if trash.length === 0} +

Trash is empty.

+ {:else} +
+ {#each trash as t (t.slug)} +
+ {t.title} + + deleted {relativeTime(t.deleted_at)} by {t.deleted_by || 'unknown'} + + +
+ {/each} +
+ {/if} +
+
+
diff --git a/web/src/lib/components/knowledge/WikiContextRail.svelte b/web/src/lib/components/knowledge/WikiContextRail.svelte new file mode 100644 index 0000000..26f56b6 --- /dev/null +++ b/web/src/lib/components/knowledge/WikiContextRail.svelte @@ -0,0 +1,124 @@ + + +
+ {#if !item} +

Nothing selected.

+ {:else} + + {#if item.about.length === 0} +

Not linked to any entity.

+ {:else} +
+ {#each item.about as slug (slug)} + + {/each} +
+ {/if} +
+ + 0} + > + {#if related.length === 0} +

No other notes share a linked entity.

+ {:else} +
+ {#each related as it (it.slug)} + {@const Icon = kindMeta(it.kind).icon} + + {/each} +
+ {/if} +
+ + + 0 && tagNeighbours.length <= 6} + > + {#if tagNeighbours.length === 0} +

No other notes share a tag.

+ {:else} +
+ {#each tagNeighbours as it (it.slug)} + {@const Icon = kindMeta(it.kind).icon} + + {/each} +
+ {/if} +
+ {/if} +
diff --git a/web/src/lib/components/knowledge/WikiNewDialog.svelte b/web/src/lib/components/knowledge/WikiNewDialog.svelte new file mode 100644 index 0000000..91e4605 --- /dev/null +++ b/web/src/lib/components/knowledge/WikiNewDialog.svelte @@ -0,0 +1,139 @@ + + + + + + New knowledge note + + +
+ {#if error} +

+ {error} +

+ {/if} + + + +
+ Type +
+ {#each KINDS as k (k)} + + {/each} +
+
+ + + + + +