508 lines
15 KiB
TypeScript
508 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { createNotesApiClient } from "@/lib/api-helpers";
|
|
import { withMutationRetry } from "@/lib/mutation-retry";
|
|
import type {
|
|
AgentTimelineItem,
|
|
ArtifactSummary,
|
|
LinkedNote,
|
|
NoteArtifactDoc,
|
|
NoteArtifactListResponse,
|
|
NoteAgentActionDoc,
|
|
NoteAgentActionListResponse,
|
|
NoteDetail,
|
|
NoteDoc,
|
|
NoteListResponse,
|
|
NoteRelationshipDoc,
|
|
NoteRelationshipListResponse,
|
|
NoteSummary,
|
|
NoteTask,
|
|
NoteTaskDoc,
|
|
NoteTaskListResponse,
|
|
WorkspaceDoc,
|
|
WorkspaceListResponse,
|
|
WorkspaceSummary,
|
|
} from "@/lib/types";
|
|
|
|
|
|
function buildWorkspaceMap(workspaces: WorkspaceDoc[]) {
|
|
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 {
|
|
return {
|
|
id: note.id,
|
|
workspaceId: note.workspaceId,
|
|
title: note.title,
|
|
excerpt: note.body.slice(0, 160),
|
|
status: note.status,
|
|
tags: note.tags,
|
|
updatedAt: note.updatedAt,
|
|
updatedBy: note.updatedBy,
|
|
};
|
|
}
|
|
|
|
function toWorkspaceSummary(workspace: WorkspaceDoc, notes: NoteDoc[]): WorkspaceSummary {
|
|
const owner = workspace.members.find((member) => member.role === "owner")?.userId ?? workspace.updatedBy;
|
|
const noteCount = notes.filter((note) => note.workspaceId === workspace.id).length;
|
|
|
|
return {
|
|
id: workspace.id,
|
|
name: workspace.name,
|
|
description: workspace.description ?? "",
|
|
owner,
|
|
noteCount,
|
|
visibility: workspace.members.length > 1 ? "shared" : "private",
|
|
updatedAt: workspace.updatedAt,
|
|
tags: [],
|
|
};
|
|
}
|
|
|
|
function toNoteTask(task: NoteTaskDoc): NoteTask {
|
|
return {
|
|
id: task.id,
|
|
title: task.title,
|
|
status:
|
|
task.status === "open"
|
|
? "todo"
|
|
: task.status === "completed"
|
|
? "done"
|
|
: "in_progress",
|
|
source: task.source === "extracted" ? "agent" : "manual",
|
|
};
|
|
}
|
|
|
|
function toArtifactSummary(artifact: NoteArtifactDoc): ArtifactSummary {
|
|
return {
|
|
id: artifact.id,
|
|
name: artifact.title,
|
|
type: artifact.artifactType,
|
|
status: artifact.description ? "ready" : "processing",
|
|
blobPath: artifact.blobPath,
|
|
contentType: artifact.contentType,
|
|
sizeBytes: artifact.sizeBytes,
|
|
};
|
|
}
|
|
|
|
function toTimelineItem(action: NoteAgentActionDoc): AgentTimelineItem {
|
|
return {
|
|
id: action.id,
|
|
actor: action.actorId,
|
|
action: `${action.actorType} ${action.actionType.replaceAll("_", " ")}`,
|
|
timestamp: action.updatedAt,
|
|
status: action.state,
|
|
summary: action.afterSummary ?? action.reason ?? action.actionType,
|
|
};
|
|
}
|
|
|
|
function toReviewState(
|
|
status: AgentTimelineItem["status"] | undefined
|
|
): NoteDetail["metadata"]["reviewState"] {
|
|
if (status === "proposed" || status === "approved" || status === "rejected") {
|
|
return status;
|
|
}
|
|
|
|
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,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
type WorkspaceSummaryDoc = WorkspaceDoc & { noteCount: number };
|
|
type WorkspaceSummaryListResponse = { items: WorkspaceSummaryDoc[]; total: number };
|
|
|
|
export async function listWorkspaceSummaries(): Promise<WorkspaceSummary[]> {
|
|
const api = createNotesApiClient();
|
|
const response = await api.fetch<WorkspaceSummaryListResponse>("/workspaces/summaries");
|
|
|
|
return response.items.map((ws) => {
|
|
const owner = ws.members.find((m) => m.role === "owner")?.userId ?? ws.updatedBy;
|
|
return {
|
|
id: ws.id,
|
|
name: ws.name,
|
|
description: ws.description ?? "",
|
|
owner,
|
|
noteCount: ws.noteCount,
|
|
visibility: ws.members.length > 1 ? "shared" : "private",
|
|
updatedAt: ws.updatedAt,
|
|
tags: [],
|
|
};
|
|
});
|
|
}
|
|
|
|
export async function listNoteSummaries(): Promise<NoteSummary[]> {
|
|
const api = createNotesApiClient();
|
|
const response = await api.fetch<NoteListResponse>("/notes");
|
|
return response.items.map(toNoteSummary);
|
|
}
|
|
|
|
export async function searchNoteSummaries(query: string): Promise<NoteSummary[]> {
|
|
const api = createNotesApiClient();
|
|
const search = query.trim();
|
|
const path = search ? `/notes?search=${encodeURIComponent(search)}` : "/notes";
|
|
const response = await api.fetch<NoteListResponse>(path);
|
|
return response.items.map(toNoteSummary);
|
|
}
|
|
|
|
export interface SearchRankedHit {
|
|
noteId: string;
|
|
workspaceId: string;
|
|
title: string;
|
|
score: number;
|
|
matchKind: string;
|
|
snippet: string;
|
|
}
|
|
|
|
export async function searchNotesRanked(
|
|
q: string,
|
|
mode: "lexical" | "hybrid",
|
|
options?: { limit?: number; offset?: number; workspaceId?: string },
|
|
): Promise<{ items: SearchRankedHit[]; mode: string; total: number }> {
|
|
const api = createNotesApiClient();
|
|
return api.fetch("/notes/search", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
q: q.trim(),
|
|
mode,
|
|
workspaceId: options?.workspaceId,
|
|
limit: options?.limit ?? 50,
|
|
offset: options?.offset ?? 0,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export async function createNoteShare(noteId: string, workspaceId: string): Promise<{ shareToken: string; path: string }> {
|
|
const api = createNotesApiClient();
|
|
return api.fetch(`/notes/${encodeURIComponent(noteId)}/share`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ workspaceId }),
|
|
});
|
|
}
|
|
|
|
export interface NoteVersionRow {
|
|
id: string;
|
|
noteId: string;
|
|
title: string;
|
|
body: string;
|
|
savedAt: string;
|
|
source: string;
|
|
}
|
|
|
|
export async function listNoteVersions(
|
|
noteId: string,
|
|
workspaceId: string,
|
|
): Promise<{ items: NoteVersionRow[]; total: number }> {
|
|
const api = createNotesApiClient();
|
|
const qs = new URLSearchParams({ workspaceId, limit: "30", offset: "0" });
|
|
return api.fetch(`/notes/${encodeURIComponent(noteId)}/versions?${qs.toString()}`);
|
|
}
|
|
|
|
export async function seedOnboardingWorkspace(): Promise<{ workspaceId: string; noteIds: string[] }> {
|
|
const api = createNotesApiClient();
|
|
return api.fetch("/workspaces/onboarding-seed", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
}
|
|
|
|
export async function chatOverWorkspace(workspaceId: string, message: string): Promise<{
|
|
answer: string;
|
|
citations: Array<{ noteId: string; title: string; snippet: string; workspaceId: string }>;
|
|
}> {
|
|
const api = createNotesApiClient();
|
|
return api.fetch("/notes/chat", {
|
|
method: "POST",
|
|
body: JSON.stringify({ workspaceId, message }),
|
|
});
|
|
}
|
|
|
|
export async function listNotesForWorkspace(workspaceId: string): Promise<NoteSummary[]> {
|
|
const api = createNotesApiClient();
|
|
const response = await api.fetch<NoteListResponse>(`/notes?workspaceId=${encodeURIComponent(workspaceId)}`);
|
|
return response.items.map(toNoteSummary);
|
|
}
|
|
|
|
export async function updateNoteDetail(
|
|
noteId: string,
|
|
workspaceId: string,
|
|
updates: {
|
|
title?: string;
|
|
body?: string;
|
|
},
|
|
): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
const path = `/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(workspaceId)}`;
|
|
await withMutationRetry({
|
|
run: () => api.fetch(path, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(updates),
|
|
}),
|
|
queue: { id: noteId, action: "patch", path, payload: updates },
|
|
});
|
|
}
|
|
|
|
export async function createNoteArtifact(input: {
|
|
id: string;
|
|
workspaceId: string;
|
|
noteId: string;
|
|
artifactType: "file" | "summary" | "extraction" | "citation" | "export";
|
|
title: string;
|
|
description?: string;
|
|
blobPath?: string;
|
|
contentType?: string;
|
|
sizeBytes?: number;
|
|
}): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
await withMutationRetry({
|
|
run: () => api.fetch("/note-artifacts", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
queue: { id: input.id, action: "post", path: "/note-artifacts", payload: input },
|
|
});
|
|
}
|
|
|
|
export async function createNoteTask(input: {
|
|
id: string;
|
|
workspaceId: string;
|
|
noteId: string;
|
|
title: string;
|
|
description?: string;
|
|
dueAt?: string;
|
|
source?: "manual" | "extracted";
|
|
}): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
await withMutationRetry({
|
|
run: () => api.fetch("/note-tasks", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
queue: { id: input.id, action: "post", path: "/note-tasks", payload: input },
|
|
});
|
|
}
|
|
|
|
export async function getNoteDetail(noteId: string, knownWorkspaceId?: string): Promise<NoteDetail | null> {
|
|
const api = createNotesApiClient();
|
|
|
|
let note: NoteDoc | undefined;
|
|
let workspaceId: string;
|
|
|
|
if (knownWorkspaceId) {
|
|
try {
|
|
note = await api.fetch<NoteDoc>(
|
|
`/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(knownWorkspaceId)}`
|
|
);
|
|
workspaceId = knownWorkspaceId;
|
|
} catch {
|
|
return null;
|
|
}
|
|
} else {
|
|
try {
|
|
const noteResponse = await api.fetch<NoteListResponse>(`/notes?search=${encodeURIComponent(noteId)}&limit=1`);
|
|
note = noteResponse.items.find((item) => item.id === noteId);
|
|
if (!note) return null;
|
|
workspaceId = note.workspaceId;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const wsId = encodeURIComponent(workspaceId);
|
|
const nId = encodeURIComponent(noteId);
|
|
|
|
const [workspaceResponse, notesForLinked, taskResponse, artifactResponse, actionResponse] = await Promise.all([
|
|
api.fetch<WorkspaceListResponse>("/workspaces"),
|
|
api.fetch<NoteListResponse>(`/notes?workspaceId=${wsId}`),
|
|
api.fetch<NoteTaskListResponse>(`/note-tasks?workspaceId=${wsId}¬eId=${nId}`),
|
|
api.fetch<NoteArtifactListResponse>(`/note-artifacts?workspaceId=${wsId}¬eId=${nId}`),
|
|
api.fetch<NoteAgentActionListResponse>(`/note-agent-actions?workspaceId=${wsId}¬eId=${nId}`),
|
|
]);
|
|
|
|
const workspaceMap = buildWorkspaceMap(workspaceResponse.items);
|
|
const noteMap = buildNoteMap(notesForLinked.items);
|
|
const workspace = workspaceMap.get(workspaceId);
|
|
|
|
let relationshipResponse: NoteRelationshipListResponse = { items: [] };
|
|
try {
|
|
const fetched = await api.fetch<NoteRelationshipListResponse>(
|
|
`/note-relationships?workspaceId=${wsId}¬eId=${nId}`
|
|
);
|
|
relationshipResponse = fetched && Array.isArray(fetched.items) ? fetched : { items: [] };
|
|
} catch {
|
|
relationshipResponse = { items: [] };
|
|
}
|
|
|
|
const tasks = taskResponse.items.map(toNoteTask);
|
|
const artifacts = artifactResponse.items.map(toArtifactSummary);
|
|
const timeline = actionResponse.items
|
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
.map(toTimelineItem);
|
|
const linkedNotes = toLinkedNotes(note.id, relationshipResponse.items, noteMap);
|
|
|
|
return {
|
|
...toNoteSummary(note),
|
|
body: note.body,
|
|
metadata: {
|
|
owner: workspace?.members.find((member) => member.role === "owner")?.userId ?? note.updatedBy,
|
|
source: note.sourceType ?? "manual",
|
|
reviewState: toReviewState(timeline[0]?.status),
|
|
taskCount: tasks.length,
|
|
artifactCount: artifacts.length,
|
|
},
|
|
linkedNotes,
|
|
tasks,
|
|
artifacts,
|
|
timeline,
|
|
};
|
|
}
|
|
|
|
export async function createNote(input: {
|
|
id: string;
|
|
workspaceId: string;
|
|
title: string;
|
|
body: string;
|
|
tags?: string[];
|
|
sourceType?: string;
|
|
}): Promise<NoteDoc> {
|
|
const api = createNotesApiClient();
|
|
return withMutationRetry({
|
|
run: () => api.fetch<NoteDoc>("/notes", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
queue: { id: input.id, action: "post", path: "/notes", payload: input },
|
|
});
|
|
}
|
|
|
|
export async function archiveNote(noteId: string, workspaceId: string): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
const path = `/notes/${encodeURIComponent(noteId)}/archive`;
|
|
await withMutationRetry({
|
|
run: () => api.fetch(path, {
|
|
method: "POST",
|
|
body: JSON.stringify({ workspaceId }),
|
|
}),
|
|
queue: { id: `${noteId}:archive`, action: "post", path, payload: { workspaceId } },
|
|
});
|
|
}
|
|
|
|
export async function restoreNote(noteId: string, workspaceId: string): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
const path = `/notes/${encodeURIComponent(noteId)}/restore`;
|
|
await withMutationRetry({
|
|
run: () => api.fetch(path, {
|
|
method: "POST",
|
|
body: JSON.stringify({ workspaceId }),
|
|
}),
|
|
queue: { id: `${noteId}:restore`, action: "post", path, payload: { workspaceId } },
|
|
});
|
|
}
|
|
|
|
export async function summarizeNote(noteId: string, workspaceId: string): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
await api.fetch(`/notes/${encodeURIComponent(noteId)}/summarize`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ workspaceId }),
|
|
});
|
|
}
|
|
|
|
export async function exportNotes(format: "json" | "markdown", workspaceId?: string): Promise<string> {
|
|
const api = createNotesApiClient();
|
|
const params = new URLSearchParams({ format });
|
|
if (workspaceId) params.set("workspaceId", workspaceId);
|
|
const res = await api.fetch<string>(`/notes/export?${params.toString()}`);
|
|
return typeof res === "string" ? res : JSON.stringify(res, null, 2);
|
|
}
|
|
|
|
export async function createWorkspace(input: {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
}): Promise<WorkspaceDoc> {
|
|
const api = createNotesApiClient();
|
|
return withMutationRetry({
|
|
run: () => api.fetch<WorkspaceDoc>("/workspaces", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
queue: { id: input.id, action: "post", path: "/workspaces", payload: input },
|
|
});
|
|
}
|
|
|
|
export async function updateWorkspace(
|
|
workspaceId: string,
|
|
updates: { name?: string; description?: string },
|
|
): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
const path = `/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
await withMutationRetry({
|
|
run: () => api.fetch(path, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(updates),
|
|
}),
|
|
queue: { id: workspaceId, action: "patch", path, payload: updates },
|
|
});
|
|
}
|
|
|
|
export async function deleteWorkspace(workspaceId: string): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
const path = `/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
await withMutationRetry({
|
|
run: () => api.fetch(path, {
|
|
method: "DELETE",
|
|
}),
|
|
queue: { id: workspaceId, action: "delete", path, payload: {} },
|
|
});
|
|
}
|
|
|
|
export async function createNoteRelationship(input: {
|
|
id: string;
|
|
workspaceId: string;
|
|
fromNoteId: string;
|
|
toNoteId: string;
|
|
relationshipType: string;
|
|
}): Promise<void> {
|
|
const api = createNotesApiClient();
|
|
await withMutationRetry({
|
|
run: () => api.fetch("/note-relationships", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
queue: { id: input.id, action: "post", path: "/note-relationships", payload: input },
|
|
});
|
|
}
|