Merge: Knowledge graph views - Entry-centered graphs and stats (closes #73, #74)

This commit is contained in:
Jason Woltje
2026-01-29 23:39:27 -06:00
25 changed files with 1701 additions and 107 deletions

View File

@@ -7,6 +7,7 @@ import { EntryStatus, Visibility } from "@mosaic/shared";
import { EntryViewer } from "@/components/knowledge/EntryViewer";
import { EntryEditor } from "@/components/knowledge/EntryEditor";
import { EntryMetadata } from "@/components/knowledge/EntryMetadata";
import { EntryGraphViewer } from "@/components/knowledge/EntryGraphViewer";
import { fetchEntry, updateEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge";
/**
@@ -20,6 +21,7 @@ export default function EntryPage() {
const [entry, setEntry] = useState<KnowledgeEntryWithTags | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [showGraph, setShowGraph] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -44,7 +46,7 @@ export default function EntryPage() {
setEditContent(data.content);
setEditStatus(data.status);
setEditVisibility(data.visibility);
setEditTags(data.tags.map((tag) => tag.id));
setEditTags(data.tags.map((tag: { id: string }) => tag.id));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load entry");
} finally {
@@ -80,7 +82,7 @@ export default function EntryPage() {
editStatus !== entry.status ||
editVisibility !== entry.visibility ||
JSON.stringify(editTags.sort()) !==
JSON.stringify(entry.tags.map((t) => t.id).sort());
JSON.stringify(entry.tags.map((t: { id: string }) => t.id).sort());
setHasUnsavedChanges(changed);
}, [entry, isEditing, editTitle, editContent, editStatus, editVisibility, editTags]);
@@ -156,7 +158,7 @@ export default function EntryPage() {
setEditContent(entry.content);
setEditStatus(entry.status);
setEditVisibility(entry.visibility);
setEditTags(entry.tags.map((tag) => tag.id));
setEditTags(entry.tags.map((tag: { id: string }) => tag.id));
setIsEditing(false);
setHasUnsavedChanges(false);
}
@@ -248,7 +250,7 @@ export default function EntryPage() {
</span>
{/* Tags */}
{entry.tags.map((tag) => (
{entry.tags.map((tag: { id: string; name: string; color: string | null }) => (
<span
key={tag.id}
className="px-3 py-1 rounded-full text-xs font-medium bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
@@ -268,10 +270,42 @@ export default function EntryPage() {
</div>
)}
{/* View Tabs */}
{!isEditing && (
<div className="mb-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex gap-4">
<button
onClick={() => setShowGraph(false)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
!showGraph
? "border-blue-500 text-blue-600 dark:text-blue-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
}`}
>
Content
</button>
<button
onClick={() => setShowGraph(true)}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
showGraph
? "border-blue-500 text-blue-600 dark:text-blue-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
}`}
>
Graph View
</button>
</div>
</div>
)}
{/* Content */}
<div className="mb-6">
{isEditing ? (
<EntryEditor content={editContent} onChange={setEditContent} />
) : showGraph ? (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden" style={{ height: '600px' }}>
<EntryGraphViewer slug={slug} initialDepth={1} />
</div>
) : (
<EntryViewer entry={entry} />
)}

View File

@@ -40,7 +40,7 @@ export default function KnowledgePage() {
// Filter by tag
if (selectedTag !== "all") {
filtered = filtered.filter((entry) =>
entry.tags.some((tag) => tag.slug === selectedTag)
entry.tags.some((tag: { slug: string }) => tag.slug === selectedTag)
);
}
@@ -51,7 +51,7 @@ export default function KnowledgePage() {
(entry) =>
entry.title.toLowerCase().includes(query) ||
entry.summary?.toLowerCase().includes(query) ||
entry.tags.some((tag) => tag.name.toLowerCase().includes(query))
entry.tags.some((tag: { name: string }) => tag.name.toLowerCase().includes(query))
);
}

View File

@@ -0,0 +1,5 @@
import { StatsDashboard } from "@/components/knowledge";
export default function KnowledgeStatsPage() {
return <StatsDashboard />;
}