learning_ai_notes/web/src/app/(app)/notes/[noteId]/page.tsx
saravanakumardb1 e5535252c7 feat(backend+web): note summarization + export endpoints [B3, B6]
- backend: POST /notes/:id/summarize — calls extraction-service, stores summary artifact
- backend: GET /notes/export — JSON + Markdown format support
- backend: extraction-client.ts for extraction-service integration
- backend: 4 new integration tests (summarize, export JSON, export MD, invalid format)
- web: summarizeNote + exportNotes client functions
- web: Summarize button on note detail page
- web: Export Notes button on workspaces page
- web: exclude e2e/ from vitest config
- Total: 80 backend, 14 web, 23 mobile = 117 tests
2026-03-19 08:59:26 -07:00

212 lines
6.9 KiB
TypeScript

"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { AppShell } from "@/components/AppShell";
import { NoteEditor } from "@/components/NoteEditor";
import { MetadataPanel } from "@/components/MetadataPanel";
import { LinkedNotesPanel } from "@/components/LinkedNotesPanel";
import { TaskReviewPanel } from "@/components/TaskReviewPanel";
import { ArtifactPanel } from "@/components/ArtifactPanel";
import { AgentTimeline } from "@/components/AgentTimeline";
import { LinkNoteModal } from "@/components/LinkNoteModal";
import { archiveNote, createNoteArtifact, createNoteTask, getNoteDetail, restoreNote, summarizeNote, updateNoteDetail } from "@/lib/notes-client";
import type { NoteDetail } from "@/lib/types";
export default function NoteDetailPage() {
const params = useParams<{ noteId: string }>();
const noteId = params.noteId;
const [note, setNote] = useState<NoteDetail | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [isCreatingTask, setIsCreatingTask] = useState(false);
const [isCreatingArtifact, setIsCreatingArtifact] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showLinkNote, setShowLinkNote] = useState(false);
useEffect(() => {
void (async () => {
try {
setNote(await getNoteDetail(noteId));
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to load note");
}
})();
}, [noteId]);
async function handleSave(updates: { title: string; body: string }) {
if (!note) {
return;
}
setIsSaving(true);
try {
await updateNoteDetail(note.id, note.workspaceId, updates);
const refreshed = await getNoteDetail(note.id);
setNote(refreshed);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to save note");
} finally {
setIsSaving(false);
}
}
async function handleCreateArtifact(input: {
title: string;
artifactType: "file" | "summary" | "extraction" | "citation" | "export";
description?: string;
blobPath?: string;
}) {
if (!note) {
return;
}
setIsCreatingArtifact(true);
try {
await createNoteArtifact({
id: crypto.randomUUID(),
workspaceId: note.workspaceId,
noteId: note.id,
artifactType: input.artifactType,
title: input.title,
description: input.description,
blobPath: input.blobPath,
});
const refreshed = await getNoteDetail(note.id);
setNote(refreshed);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to create artifact");
} finally {
setIsCreatingArtifact(false);
}
}
async function handleCreateTask(input: { title: string; description?: string }) {
if (!note) {
return;
}
setIsCreatingTask(true);
try {
await createNoteTask({
id: crypto.randomUUID(),
workspaceId: note.workspaceId,
noteId: note.id,
title: input.title,
description: input.description,
source: "manual",
});
const refreshed = await getNoteDetail(note.id);
setNote(refreshed);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to create task");
} finally {
setIsCreatingTask(false);
}
}
async function handleSummarize() {
if (!note) return;
try {
await summarizeNote(note.id, note.workspaceId);
setNote(await getNoteDetail(note.id, note.workspaceId));
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to summarize note");
}
}
async function handleArchive() {
if (!note) return;
try {
await archiveNote(note.id, note.workspaceId);
setNote(await getNoteDetail(note.id, note.workspaceId));
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to archive note");
}
}
async function handleRestore() {
if (!note) return;
try {
await restoreNote(note.id, note.workspaceId);
setNote(await getNoteDetail(note.id, note.workspaceId));
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to restore note");
}
}
if (!note) {
return (
<AppShell
title="Note detail"
description="Editable note surface with metadata, linked context, tasks, artifacts, and review history."
actions={<div className="badge">Loading</div>}
>
<section className="surface-card" style={{ padding: "var(--ml-space-6)", color: "var(--ml-text-secondary)" }}>
{error ?? "Loading note…"}
</section>
</AppShell>
);
}
return (
<AppShell
title={note.title}
description="Editable note surface with metadata, linked context, tasks, artifacts, and review history."
actions={
<div style={{ display: "flex", gap: "var(--ml-space-2)", alignItems: "center" }}>
{isSaving ? (
<div className="badge">Saving</div>
) : (
<Link href="/reviews" className="badge">
{`Review: ${note.metadata.reviewState}`}
</Link>
)}
<button className="btn btn-secondary" onClick={handleSummarize}>
Summarize
</button>
<button className="btn btn-secondary" onClick={() => setShowLinkNote(true)}>
Link Note
</button>
{note.status === "archived" ? (
<button className="btn btn-primary" onClick={handleRestore}>Restore</button>
) : (
<button className="btn btn-secondary" onClick={handleArchive}>Archive</button>
)}
</div>
}
>
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.7fr) minmax(320px, 1fr)", gap: "var(--ml-space-4)" }}>
<NoteEditor note={note} onSave={handleSave} isSaving={isSaving} />
<aside style={{ display: "grid", gap: "var(--ml-space-4)" }}>
{error ? <div className="surface-card" style={{ padding: "var(--ml-space-4)", color: "var(--ml-text-secondary)" }}>{error}</div> : null}
<MetadataPanel note={note} />
<LinkedNotesPanel linkedNotes={note.linkedNotes} />
<TaskReviewPanel tasks={note.tasks} onCreate={handleCreateTask} isCreating={isCreatingTask} />
<ArtifactPanel artifacts={note.artifacts} onCreate={handleCreateArtifact} isCreating={isCreatingArtifact} />
<AgentTimeline items={note.timeline} />
</aside>
</div>
{showLinkNote && (
<LinkNoteModal
noteId={note.id}
workspaceId={note.workspaceId}
existingLinkedIds={note.linkedNotes.map((ln) => ln.id)}
onLinked={() => {
setShowLinkNote(false);
void getNoteDetail(note.id, note.workspaceId).then(setNote);
}}
onClose={() => setShowLinkNote(false)}
/>
)}
</AppShell>
);
}