Phase 1: Command palette (⌘K), editor autosave with quiet auto-saves, dashboard saved views from API + quick links + onboarding seed CTA, explicit task scan panel. Phase 2: Context pack formatter with YAML frontmatter, copy on note + workspace .md export. Phase 3: ADR for hybrid search without embeddings; POST /notes/search (lexical + ranked hybrid); search UI mode toggle. Phase 4: POST copilot + suggest-title; in-editor copilot actions; /chat retrieval answers with citations (backend chat.rag_enabled). Phase 5: Settings MCP snippet, offline queue note, API token deferral; DEEP_LINKS.md. Phase 6: Note shares + public GET; share page; POST onboarding-seed. Phase 7: note_versions on PATCH; version panel; create-note templates; PWA manifest. Flags: search.hybrid_enabled, copilot.enabled, chat.rag_enabled, onboarding.seed_enabled. Made-with: Cursor
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { listNoteVersions, type NoteVersionRow } from "@/lib/notes-client";
|
|
|
|
export function NoteVersionsPanel({ noteId, workspaceId }: { noteId: string; workspaceId: string }) {
|
|
const [items, setItems] = useState<NoteVersionRow[]>([]);
|
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
try {
|
|
const res = await listNoteVersions(noteId, workspaceId);
|
|
setItems(res.items);
|
|
setError(null);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "Could not load versions");
|
|
}
|
|
})();
|
|
}, [noteId, workspaceId]);
|
|
|
|
if (error) {
|
|
return (
|
|
<section className="surface-card" style={{ padding: "var(--nl-space-4)", color: "var(--nl-text-secondary)" }}>
|
|
{error}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<section className="surface-card" style={{ padding: "var(--nl-space-4)", color: "var(--nl-text-secondary)" }}>
|
|
No saved versions yet. Versions are created when you edit title or body.
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
|
<div style={{ fontWeight: 700 }}>Version history</div>
|
|
<ul style={{ listStyle: "none", margin: 0, padding: 0, display: "grid", gap: "var(--nl-space-2)" }}>
|
|
{items.map((v) => (
|
|
<li key={v.id} className="surface-muted" style={{ padding: "var(--nl-space-3)" }}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpenId((o) => (o === v.id ? null : v.id))}
|
|
style={{
|
|
width: "100%",
|
|
textAlign: "left",
|
|
background: "none",
|
|
border: "none",
|
|
color: "var(--nl-text-primary)",
|
|
cursor: "pointer",
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{new Date(v.savedAt).toLocaleString()} · {v.source} · {v.title.slice(0, 48)}
|
|
{v.title.length > 48 ? "…" : ""}
|
|
</button>
|
|
{openId === v.id ? (
|
|
<pre
|
|
style={{
|
|
marginTop: 8,
|
|
whiteSpace: "pre-wrap",
|
|
fontSize: "var(--nl-fs-sm)",
|
|
color: "var(--nl-text-secondary)",
|
|
maxHeight: 200,
|
|
overflow: "auto",
|
|
}}
|
|
>
|
|
{v.body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()}
|
|
</pre>
|
|
) : null}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|