feat(notes): wire backend-backed note workspace slice

This commit is contained in:
saravanakumardb1 2026-03-10 15:53:57 -07:00
parent 418ecaacc5
commit 5995b6c725
8 changed files with 251 additions and 46 deletions

View File

@ -24,6 +24,23 @@ type NoteListResponse = {
items: NoteDoc[]; items: NoteDoc[];
}; };
type CreateNoteInput = {
id: string;
workspaceId: string;
title: string;
body: string;
tags?: string[];
links?: string[];
};
function generateNoteId(): string {
if (typeof globalThis.crypto?.randomUUID === 'function') {
return globalThis.crypto.randomUUID();
}
return `note-${Date.now()}`;
}
function mapWorkspaceNames(workspaces: MobileWorkspace[]) { function mapWorkspaceNames(workspaces: MobileWorkspace[]) {
return new Map(workspaces.map((workspace) => [workspace.id, workspace.name])); return new Map(workspaces.map((workspace) => [workspace.id, workspace.name]));
} }
@ -59,6 +76,32 @@ export async function getNote(id: string, workspaceId: string): Promise<MobileNo
return toMobileNote(note, mapWorkspaceNames(workspaces)); return toMobileNote(note, mapWorkspaceNames(workspaces));
} }
export async function createNote(
workspaceId: string,
title: string,
body: string,
): Promise<MobileNote> {
const trimmedTitle = title.trim() || 'Untitled draft';
const payload: CreateNoteInput = {
id: generateNoteId(),
workspaceId,
title: trimmedTitle,
body,
tags: [],
links: [],
};
const [created, workspaces] = await Promise.all([
getApiClient().fetch<NoteDoc>('/notes', {
method: 'POST',
body: JSON.stringify(payload),
}),
listWorkspaces(),
]);
return toMobileNote(created, mapWorkspaceNames(workspaces));
}
export async function updateNote( export async function updateNote(
id: string, id: string,
workspaceId: string, workspaceId: string,

View File

@ -45,9 +45,13 @@ export default function CaptureScreen() {
textAlignVertical="top" textAlignVertical="top"
/> />
<Pressable <Pressable
onPress={() => { onPress={async () => {
saveDraft(title, body); const didSave = await saveDraft(activeWorkspaceId, title, body);
setSaved(true); setSaved(didSave);
if (!didSave) {
return;
}
setTitle(''); setTitle('');
setBody(''); setBody('');
}} }}
@ -55,7 +59,7 @@ export default function CaptureScreen() {
> >
<Text style={styles.buttonText}>Save draft</Text> <Text style={styles.buttonText}>Save draft</Text>
</Pressable> </Pressable>
{saved ? <Text style={styles.saved}>Draft saved locally in this scaffold step.</Text> : null} {saved ? <Text style={styles.saved}>Draft saved to the product backend.</Text> : null}
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.cardTitle}>Offline queue readiness</Text> <Text style={styles.cardTitle}>Offline queue readiness</Text>
<Text style={styles.cardBody}>Queue capacity: {OFFLINE_QUEUE_MAX_SIZE} items</Text> <Text style={styles.cardBody}>Queue capacity: {OFFLINE_QUEUE_MAX_SIZE} items</Text>

View File

@ -1,5 +1,5 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { getNote, listNotes, updateNote as persistNote, type MobileNote } from '../api/notes'; import { createNote as persistNewNote, getNote, listNotes, updateNote as persistNote, type MobileNote } from '../api/notes';
export type NotesState = { export type NotesState = {
notes: MobileNote[]; notes: MobileNote[];
@ -7,7 +7,7 @@ export type NotesState = {
isLoading: boolean; isLoading: boolean;
hydrate: () => Promise<void>; hydrate: () => Promise<void>;
openNote: (id: string) => Promise<void>; openNote: (id: string) => Promise<void>;
saveDraft: (title: string, body: string) => void; saveDraft: (workspaceId: string | null, title: string, body: string) => Promise<boolean>;
updateNote: (id: string, title: string, body: string) => Promise<void>; updateNote: (id: string, title: string, body: string) => Promise<void>;
}; };
@ -31,18 +31,19 @@ export const useNotesStore = create<NotesState>((set, get) => ({
const note = await getNote(id, current.workspaceId); const note = await getNote(id, current.workspaceId);
set({ selectedNote: note, isLoading: false }); set({ selectedNote: note, isLoading: false });
}, },
saveDraft(title: string, body: string) { async saveDraft(workspaceId: string | null, title: string, body: string) {
const draft: MobileNote = { if (!workspaceId) {
id: `draft-${Date.now()}`, return false;
workspaceId: 'drafts', }
title: title.trim() || 'Untitled draft',
body,
workspaceName: 'Drafts',
status: 'draft',
updatedAt: new Date().toISOString(),
};
set({ notes: [draft, ...get().notes], selectedNote: draft }); set({ isLoading: true });
const created = await persistNewNote(workspaceId, title, body);
set({
notes: [created, ...get().notes],
selectedNote: created,
isLoading: false,
});
return true;
}, },
async updateNote(id: string, title: string, body: string) { async updateNote(id: string, title: string, body: string) {
const nextTitle = title.trim() || 'Untitled draft'; const nextTitle = title.trim() || 'Untitled draft';

View File

@ -3,29 +3,78 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AppShell } from "@/components/AppShell"; import { AppShell } from "@/components/AppShell";
import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client"; import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client";
import { mockOperatorWorkflows, mockSavedViews } from "@/lib/mock-data"; import { listApprovalQueue } from "@/lib/review-client";
import type { NoteSummary, WorkspaceSummary } from "@/lib/types"; import type { NoteSummary, WorkspaceSummary } from "@/lib/types";
export default function DashboardPage() { export default function DashboardPage() {
const [notes, setNotes] = useState<NoteSummary[]>([]); const [notes, setNotes] = useState<NoteSummary[]>([]);
const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]); const [workspaces, setWorkspaces] = useState<WorkspaceSummary[]>([]);
const [pendingReviewCount, setPendingReviewCount] = useState(0);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
void (async () => { void (async () => {
try { try {
const [nextNotes, nextWorkspaces] = await Promise.all([ const [nextNotes, nextWorkspaces, nextApprovalQueue] = await Promise.all([
listNoteSummaries(), listNoteSummaries(),
listWorkspaceSummaries(), listWorkspaceSummaries(),
listApprovalQueue(),
]); ]);
setNotes(nextNotes); setNotes(nextNotes);
setWorkspaces(nextWorkspaces); setWorkspaces(nextWorkspaces);
setPendingReviewCount(nextApprovalQueue.length);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Unable to load dashboard data"); setError(err instanceof Error ? err.message : "Unable to load dashboard data");
} }
})(); })();
}, []); }, []);
const savedViews = [
{
id: "workspace-all",
name: "All workspaces",
scope: "workspace",
description: "Current workspace inventory derived from backend-backed workspace data.",
query: "visibility:any sort:updated",
resultCount: workspaces.length,
},
{
id: "draft-notes",
name: "Draft notes",
scope: "search",
description: "Draft notes currently tracked across the active knowledge base.",
query: "status:draft",
resultCount: notes.filter((note) => note.status === "draft").length,
},
{
id: "pending-review",
name: "Pending review",
scope: "review",
description: "Current agent-mediated items awaiting review.",
query: "state:draft|proposed",
resultCount: pendingReviewCount,
},
];
const operatorWorkflows = [
{
id: "workflow-approvals",
name: "Approval triage",
owner: "Operator",
queueCount: pendingReviewCount,
sla: "< 4h",
status: pendingReviewCount > 3 ? "at_risk" : "healthy",
},
{
id: "workflow-workspaces",
name: "Workspace coverage",
owner: "Knowledge Ops",
queueCount: workspaces.length,
sla: "< 1d",
status: workspaces.length > 5 ? "at_risk" : "healthy",
},
] as const;
const recentNotes = notes.slice(0, 3); const recentNotes = notes.slice(0, 3);
return ( return (
@ -45,7 +94,7 @@ export default function DashboardPage() {
</div> </div>
<div className="surface-card" style={{ padding: "var(--ml-space-5)" }}> <div className="surface-card" style={{ padding: "var(--ml-space-5)" }}>
<div style={{ color: "var(--ml-text-secondary)" }}>Pending review surfaces</div> <div style={{ color: "var(--ml-text-secondary)" }}>Pending review surfaces</div>
<div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>2</div> <div style={{ fontSize: "var(--ml-fs-3xl)", fontWeight: 700, marginTop: "var(--ml-space-2)" }}>{pendingReviewCount}</div>
</div> </div>
</section> </section>
@ -53,7 +102,7 @@ export default function DashboardPage() {
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}> <section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Saved views</div> <div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Saved views</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}> <div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockSavedViews.map((view) => ( {savedViews.map((view) => (
<article key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}> <article key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}> <div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
<strong>{view.name}</strong> <strong>{view.name}</strong>
@ -72,7 +121,7 @@ export default function DashboardPage() {
<section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}> <section className="surface-card" style={{ padding: "var(--ml-space-6)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Operator workflows</div> <div style={{ fontSize: "var(--ml-fs-xl)", fontWeight: 700 }}>Operator workflows</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}> <div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockOperatorWorkflows.map((workflow) => ( {operatorWorkflows.map((workflow) => (
<article key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}> <article key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}> <div style={{ display: "flex", justifyContent: "space-between", gap: "var(--ml-space-3)", flexWrap: "wrap" }}>
<strong>{workflow.name}</strong> <strong>{workflow.name}</strong>

View File

@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell"; import { AppShell } from "@/components/AppShell";
import { AgentTimeline } from "@/components/AgentTimeline"; import { AgentTimeline } from "@/components/AgentTimeline";
import { ProposalReviewCard } from "@/components/ProposalReviewCard"; import { ProposalReviewCard } from "@/components/ProposalReviewCard";
import { mockOperatorWorkflows } from "@/lib/mock-data";
import { listAgentTimeline, listApprovalQueue } from "@/lib/review-client"; import { listAgentTimeline, listApprovalQueue } from "@/lib/review-client";
import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types"; import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types";
@ -29,6 +28,24 @@ export default function ReviewsPage() {
}, []); }, []);
const featuredProposal = useMemo(() => approvalQueue[0] ?? null, [approvalQueue]); const featuredProposal = useMemo(() => approvalQueue[0] ?? null, [approvalQueue]);
const operatorWorkflows = [
{
id: "workflow-approvals",
name: "Approval triage",
owner: "Operator",
queueCount: approvalQueue.length,
sla: "< 4h",
status: approvalQueue.length > 3 ? "at_risk" : "healthy",
},
{
id: "workflow-agent-activity",
name: "Agent activity review",
owner: "Knowledge Ops",
queueCount: timeline.length,
sla: "< 1d",
status: timeline.length > 6 ? "at_risk" : "healthy",
},
] as const;
return ( return (
<AppShell <AppShell
@ -40,7 +57,7 @@ export default function ReviewsPage() {
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}> <aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontWeight: 700 }}>Operator workflows</div> <div style={{ fontWeight: 700 }}>Operator workflows</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}> <div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockOperatorWorkflows.map((workflow) => ( {operatorWorkflows.map((workflow) => (
<div key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}> <div key={workflow.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
<strong>{workflow.name}</strong> <strong>{workflow.name}</strong>
<span style={{ color: "var(--ml-text-secondary)" }}>Owner: {workflow.owner}</span> <span style={{ color: "var(--ml-text-secondary)" }}>Owner: {workflow.owner}</span>

View File

@ -4,7 +4,6 @@ import Link from "next/link";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell"; import { AppShell } from "@/components/AppShell";
import { listNoteSummaries } from "@/lib/notes-client"; import { listNoteSummaries } from "@/lib/notes-client";
import { mockSavedViews } from "@/lib/mock-data";
import type { NoteSummary } from "@/lib/types"; import type { NoteSummary } from "@/lib/types";
export default function SearchPage() { export default function SearchPage() {
@ -35,6 +34,25 @@ export default function SearchPage() {
); );
}, [notes, query]); }, [notes, query]);
const savedViews = [
{
id: "search-launch-readiness",
name: "Launch readiness",
query: "tag:launch tag:mvp status:active",
resultCount: notes.filter(
(note) =>
note.status === "active" &&
note.tags.some((tag) => tag === "launch" || tag === "mvp")
).length,
},
{
id: "search-drafts",
name: "Draft notes",
query: "status:draft",
resultCount: notes.filter((note) => note.status === "draft").length,
},
];
return ( return (
<AppShell <AppShell
title="Search" title="Search"
@ -45,15 +63,13 @@ export default function SearchPage() {
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}> <aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontWeight: 700 }}>Saved searches</div> <div style={{ fontWeight: 700 }}>Saved searches</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}> <div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockSavedViews {savedViews.map((view) => (
.filter((view) => view.scope === "search") <div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
.map((view) => ( <strong>{view.name}</strong>
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}> <span style={{ color: "var(--ml-text-secondary)" }}>{view.query}</span>
<strong>{view.name}</strong> <span className="badge">{view.resultCount} results</span>
<span style={{ color: "var(--ml-text-secondary)" }}>{view.query}</span> </div>
<span className="badge">{view.resultCount} results</span> ))}
</div>
))}
</div> </div>
<div style={{ display: "grid", gap: "var(--ml-space-2)" }}> <div style={{ display: "grid", gap: "var(--ml-space-2)" }}>

View File

@ -4,7 +4,6 @@ import Link from "next/link";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell"; import { AppShell } from "@/components/AppShell";
import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client"; import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client";
import { mockSavedViews } from "@/lib/mock-data";
import type { NoteSummary, WorkspaceSummary } from "@/lib/types"; import type { NoteSummary, WorkspaceSummary } from "@/lib/types";
export default function WorkspacesPage() { export default function WorkspacesPage() {
@ -38,6 +37,21 @@ export default function WorkspacesPage() {
[notes, workspaces], [notes, workspaces],
); );
const savedViews = [
{
id: "workspace-all",
name: "All workspaces",
description: "Current backend-backed workspace inventory.",
resultCount: workspaces.length,
},
{
id: "workspace-shared",
name: "Shared workspaces",
description: "Workspaces with more than one member.",
resultCount: workspaces.filter((workspace) => workspace.visibility === "shared").length,
},
];
return ( return (
<AppShell <AppShell
title="Workspaces" title="Workspaces"
@ -48,15 +62,13 @@ export default function WorkspacesPage() {
<aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}> <aside className="surface-card" style={{ padding: "var(--ml-space-5)", display: "grid", gap: "var(--ml-space-4)" }}>
<div style={{ fontWeight: 700 }}>Saved views</div> <div style={{ fontWeight: 700 }}>Saved views</div>
<div style={{ display: "grid", gap: "var(--ml-space-3)" }}> <div style={{ display: "grid", gap: "var(--ml-space-3)" }}>
{mockSavedViews {savedViews.map((view) => (
.filter((view) => view.scope === "workspace") <div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}>
.map((view) => ( <strong>{view.name}</strong>
<div key={view.id} className="surface-muted" style={{ padding: "var(--ml-space-4)", display: "grid", gap: "var(--ml-space-2)" }}> <span style={{ color: "var(--ml-text-secondary)" }}>{view.description}</span>
<strong>{view.name}</strong> <span className="badge">{view.resultCount} results</span>
<span style={{ color: "var(--ml-text-secondary)" }}>{view.description}</span> </div>
<span className="badge">{view.resultCount} results</span> ))}
</div>
))}
</div> </div>
</aside> </aside>

View File

@ -1,7 +1,7 @@
import { createApiClient } from "@bytelyst/api-client"; import { createApiClient } from "@bytelyst/api-client";
import { extractSuggestedTasks } from "@/lib/extraction-client"; import { extractSuggestedTasks } from "@/lib/extraction-client";
import { NOTES_API_URL, PRODUCT_ID } from "@/lib/product-config"; import { NOTES_API_URL, PRODUCT_ID } from "@/lib/product-config";
import type { AgentTimelineItem, ArtifactSummary, NoteDetail, NoteSummary, NoteTask, WorkspaceSummary } from "@/lib/types"; import type { AgentTimelineItem, ArtifactSummary, LinkedNote, NoteDetail, NoteSummary, NoteTask, WorkspaceSummary } from "@/lib/types";
type NoteDoc = { type NoteDoc = {
id: string; id: string;
@ -78,6 +78,18 @@ type NoteAgentActionListResponse = {
items: NoteAgentActionDoc[]; items: NoteAgentActionDoc[];
}; };
type NoteRelationshipDoc = {
id: string;
workspaceId: string;
fromNoteId: string;
toNoteId: string;
relationshipType: string;
};
type NoteRelationshipListResponse = {
items: NoteRelationshipDoc[];
};
function getAccessToken(): string | null { function getAccessToken(): string | null {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return null; return null;
@ -100,6 +112,10 @@ function buildWorkspaceMap(workspaces: WorkspaceDoc[]) {
return new Map(workspaces.map((workspace) => [workspace.id, workspace])); return new Map(workspaces.map((workspace) => [workspace.id, workspace]));
} }
function buildNoteMap(notes: NoteDoc[]) {
return new Map(notes.map((note) => [note.id, note]));
}
function toNoteSummary(note: NoteDoc): NoteSummary { function toNoteSummary(note: NoteDoc): NoteSummary {
return { return {
id: note.id, id: note.id,
@ -176,6 +192,38 @@ function toReviewState(
return "none"; return "none";
} }
function toLinkedNotes(
noteId: string,
relationships: NoteRelationshipDoc[],
noteMap: Map<string, NoteDoc>,
): LinkedNote[] {
return relationships.flatMap((relationship) => {
const relatedNoteId =
relationship.fromNoteId === noteId
? relationship.toNoteId
: relationship.toNoteId === noteId
? relationship.fromNoteId
: null;
if (!relatedNoteId) {
return [];
}
const relatedNote = noteMap.get(relatedNoteId);
if (!relatedNote) {
return [];
}
return [
{
id: relatedNote.id,
title: relatedNote.title,
relationship: relationship.relationshipType,
},
];
});
}
export async function listWorkspaceSummaries(): Promise<WorkspaceSummary[]> { export async function listWorkspaceSummaries(): Promise<WorkspaceSummary[]> {
const api = createNotesApiClient(); const api = createNotesApiClient();
const [workspaceResponse, noteResponse] = await Promise.all([ const [workspaceResponse, noteResponse] = await Promise.all([
@ -211,6 +259,7 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
} }
const workspaceMap = buildWorkspaceMap(workspaceResponse.items); const workspaceMap = buildWorkspaceMap(workspaceResponse.items);
const noteMap = buildNoteMap(noteResponse.items);
const workspace = workspaceMap.get(note.workspaceId); const workspace = workspaceMap.get(note.workspaceId);
const [taskResponse, artifactResponse, actionResponse] = await Promise.all([ const [taskResponse, artifactResponse, actionResponse] = await Promise.all([
api.fetch<NoteTaskListResponse>( api.fetch<NoteTaskListResponse>(
@ -223,6 +272,19 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
`/note-agent-actions?workspaceId=${encodeURIComponent(note.workspaceId)}&noteId=${encodeURIComponent(note.id)}` `/note-agent-actions?workspaceId=${encodeURIComponent(note.workspaceId)}&noteId=${encodeURIComponent(note.id)}`
), ),
]); ]);
let relationshipResponse: NoteRelationshipListResponse = { items: [] };
try {
const fetchedRelationships = await api.fetch<NoteRelationshipListResponse>(
`/note-relationships?workspaceId=${encodeURIComponent(note.workspaceId)}&noteId=${encodeURIComponent(note.id)}`
);
relationshipResponse =
fetchedRelationships && Array.isArray(fetchedRelationships.items)
? fetchedRelationships
: { items: [] };
} catch {
relationshipResponse = { items: [] };
}
const extractedTasks = await extractSuggestedTasks(note.body).catch(() => []); const extractedTasks = await extractSuggestedTasks(note.body).catch(() => []);
const existingTaskTitles = new Set(taskResponse.items.map((task) => task.title.trim().toLowerCase())); const existingTaskTitles = new Set(taskResponse.items.map((task) => task.title.trim().toLowerCase()));
@ -234,6 +296,7 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
const timeline = actionResponse.items const timeline = actionResponse.items
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.map(toTimelineItem); .map(toTimelineItem);
const linkedNotes = toLinkedNotes(note.id, relationshipResponse.items, noteMap);
return { return {
...toNoteSummary(note), ...toNoteSummary(note),
@ -245,7 +308,7 @@ export async function getNoteDetail(noteId: string): Promise<NoteDetail | null>
taskCount: tasks.length, taskCount: tasks.length,
artifactCount: artifacts.length, artifactCount: artifacts.length,
}, },
linkedNotes: [], linkedNotes,
tasks, tasks,
artifacts, artifacts,
timeline, timeline,