feat(notes): persist web note edits

This commit is contained in:
saravanakumardb1 2026-03-10 16:14:22 -07:00
parent 3f54332422
commit cdc03e3541
3 changed files with 108 additions and 21 deletions

View File

@ -9,13 +9,14 @@ import { LinkedNotesPanel } from "@/components/LinkedNotesPanel";
import { TaskReviewPanel } from "@/components/TaskReviewPanel";
import { ArtifactPanel } from "@/components/ArtifactPanel";
import { AgentTimeline } from "@/components/AgentTimeline";
import { getNoteDetail } from "@/lib/notes-client";
import { getNoteDetail, 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 [error, setError] = useState<string | null>(null);
useEffect(() => {
@ -28,6 +29,25 @@ export default function NoteDetailPage() {
})();
}, [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);
}
}
if (!note) {
return (
<AppShell
@ -46,12 +66,13 @@ export default function NoteDetailPage() {
<AppShell
title={note.title}
description="Editable note surface with metadata, linked context, tasks, and artifact placeholders."
actions={<div className="badge">Review: {note.metadata.reviewState}</div>}
actions={<div className="badge">{isSaving ? "Saving" : `Review: ${note.metadata.reviewState}`}</div>}
>
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.7fr) minmax(320px, 1fr)", gap: "var(--ml-space-4)" }}>
<NoteEditor note={note} />
<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} />

View File

@ -1,26 +1,74 @@
import { useEffect, useState } from "react";
import type { NoteDetail } from "@/lib/types";
export function NoteEditor({ note }: { note: NoteDetail }) {
export function NoteEditor({
note,
onSave,
isSaving = false,
}: {
note: NoteDetail;
onSave: (updates: { title: string; body: string }) => Promise<void>;
isSaving?: boolean;
}) {
const [title, setTitle] = useState(note.title);
const [body, setBody] = useState(note.body);
useEffect(() => {
setTitle(note.title);
setBody(note.body);
}, [note.body, note.title]);
return (
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
<label htmlFor="note-title" style={{ color: "var(--ml-text-secondary)" }}>
Title
</label>
<input id="note-title" className="input-shell" defaultValue={note.title} />
</div>
<form
style={{ display: "grid", gap: "var(--ml-space-4)" }}
onSubmit={(event) => {
event.preventDefault();
void onSave({ title, body });
}}
>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
<label htmlFor="note-title" style={{ color: "var(--ml-text-secondary)" }}>
Title
</label>
<input
id="note-title"
className="input-shell"
value={title}
onChange={(event) => setTitle(event.target.value)}
/>
</div>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
<label htmlFor="note-body" style={{ color: "var(--ml-text-secondary)" }}>
Body
</label>
<textarea
id="note-body"
className="input-shell"
defaultValue={note.body}
style={{ minHeight: 360, resize: "vertical" }}
/>
</div>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}>
<label htmlFor="note-body" style={{ color: "var(--ml-text-secondary)" }}>
Body
</label>
<textarea
id="note-body"
className="input-shell"
value={body}
onChange={(event) => setBody(event.target.value)}
style={{ minHeight: 360, resize: "vertical" }}
/>
</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<button
type="submit"
disabled={isSaving}
style={{
border: "none",
borderRadius: "var(--ml-radius-md)",
padding: "10px 14px",
background: "var(--ml-accent-primary)",
color: "var(--ml-text-primary)",
fontWeight: 600,
}}
>
{isSaving ? "Saving…" : "Save note"}
</button>
</div>
</form>
</section>
);
}

View File

@ -246,6 +246,24 @@ export async function listNotesForWorkspace(workspaceId: string): Promise<NoteSu
return response.items.map(toNoteSummary);
}
export async function updateNoteDetail(
noteId: string,
workspaceId: string,
updates: {
title?: string;
body?: string;
},
): Promise<void> {
const api = createNotesApiClient();
await api.fetch(
`/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
{
method: "PATCH",
body: JSON.stringify(updates),
},
);
}
export async function getNoteDetail(noteId: string): Promise<NoteDetail | null> {
const api = createNotesApiClient();
const [workspaceResponse, noteResponse] = await Promise.all([