{item.title}
@@ -55,13 +81,15 @@ export default function ReviewsPage() {
-
+ {featuredProposal ? (
+
+ ) : null}
-
+
);
}
diff --git a/web/src/components/AgentTimeline.tsx b/web/src/components/AgentTimeline.tsx
index d585526..ee5e0f8 100644
--- a/web/src/components/AgentTimeline.tsx
+++ b/web/src/components/AgentTimeline.tsx
@@ -1,11 +1,4 @@
-export interface AgentTimelineItem {
- id: string;
- actor: string;
- action: string;
- timestamp: string;
- status: "draft" | "proposed" | "approved" | "rejected" | "applied";
- summary: string;
-}
+import type { AgentTimelineItem } from "@/lib/types";
export function AgentTimeline({ items }: { items: AgentTimelineItem[] }) {
return (
diff --git a/web/src/lib/mock-data.ts b/web/src/lib/mock-data.ts
index 1a3bc9b..ee44b82 100644
--- a/web/src/lib/mock-data.ts
+++ b/web/src/lib/mock-data.ts
@@ -1,4 +1,5 @@
import type {
+ AgentTimelineItem,
NoteDetail,
NoteSummary,
OperatorWorkflowSummary,
@@ -136,6 +137,33 @@ export const mockOperatorWorkflows: OperatorWorkflowSummary[] = [
},
];
+const mockTimeline: AgentTimelineItem[] = [
+ {
+ id: "event-1",
+ actor: "Research Agent",
+ action: "Proposed metadata enrichment",
+ timestamp: "2026-03-10 08:11",
+ status: "proposed",
+ summary: "Suggested new tags, source metadata, and a research confidence note.",
+ },
+ {
+ id: "event-2",
+ actor: "Workflow Agent",
+ action: "Extracted task candidates",
+ timestamp: "2026-03-10 08:08",
+ status: "draft",
+ summary: "Produced three task candidates linked to the current note body.",
+ },
+ {
+ id: "event-3",
+ actor: "Operator",
+ action: "Approved editorial summary",
+ timestamp: "2026-03-10 08:03",
+ status: "approved",
+ summary: "Accepted a summary rewrite and preserved original source wording in history.",
+ },
+];
+
export const mockNoteDetails: Record
= {
"note-prd-cutline": {
...mockNotes[0],
@@ -160,6 +188,7 @@ export const mockNoteDetails: Record = {
{ id: "artifact-prd", name: "PRD v2.0", type: "document", status: "ready" },
{ id: "artifact-roadmap", name: "Roadmap execution sheet", type: "document", status: "ready" },
],
+ timeline: mockTimeline,
},
"note-web-shell": {
...mockNotes[1],
@@ -181,6 +210,7 @@ export const mockNoteDetails: Record = {
artifacts: [
{ id: "artifact-shell", name: "UI scaffold", type: "ui", status: "processing" },
],
+ timeline: mockTimeline,
},
};
diff --git a/web/src/lib/notes-client.ts b/web/src/lib/notes-client.ts
index b7379e7..fa27656 100644
--- a/web/src/lib/notes-client.ts
+++ b/web/src/lib/notes-client.ts
@@ -1,6 +1,6 @@
import { createApiClient } from "@bytelyst/api-client";
import { NOTES_API_URL, PRODUCT_ID } from "@/lib/product-config";
-import type { NoteDetail, NoteSummary, WorkspaceSummary } from "@/lib/types";
+import type { AgentTimelineItem, ArtifactSummary, NoteDetail, NoteSummary, NoteTask, WorkspaceSummary } from "@/lib/types";
type NoteDoc = {
id: string;
@@ -32,6 +32,48 @@ type WorkspaceListResponse = {
items: WorkspaceDoc[];
};
+type NoteTaskDoc = {
+ id: string;
+ noteId: string;
+ title: string;
+ description?: string;
+ status: "open" | "in_progress" | "completed" | "canceled";
+ source: "manual" | "extracted";
+};
+
+type NoteArtifactDoc = {
+ id: string;
+ noteId: string;
+ artifactType: "file" | "summary" | "extraction" | "citation" | "export";
+ title: string;
+ description?: string;
+};
+
+type NoteAgentActionDoc = {
+ id: string;
+ noteId: string;
+ actorId: string;
+ actorType: "agent" | "human";
+ actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation";
+ state: "draft" | "proposed" | "approved" | "rejected" | "applied";
+ reason?: string;
+ beforeSummary?: string;
+ afterSummary?: string;
+ updatedAt: string;
+};
+
+type NoteTaskListResponse = {
+ items: NoteTaskDoc[];
+};
+
+type NoteArtifactListResponse = {
+ items: NoteArtifactDoc[];
+};
+
+type NoteAgentActionListResponse = {
+ items: NoteAgentActionDoc[];
+};
+
function getAccessToken(): string | null {
if (typeof window === "undefined") {
return null;
@@ -83,6 +125,50 @@ function toWorkspaceSummary(workspace: WorkspaceDoc, notes: NoteDoc[]): Workspac
};
}
+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",
+ };
+}
+
+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";
+}
+
export async function listWorkspaceSummaries(): Promise {
const api = createNotesApiClient();
const [workspaceResponse, noteResponse] = await Promise.all([
@@ -119,6 +205,23 @@ export async function getNoteDetail(noteId: string): Promise
const workspaceMap = buildWorkspaceMap(workspaceResponse.items);
const workspace = workspaceMap.get(note.workspaceId);
+ const [taskResponse, artifactResponse, actionResponse] = await Promise.all([
+ api.fetch(
+ `/note-tasks?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
+ ),
+ api.fetch(
+ `/note-artifacts?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
+ ),
+ api.fetch(
+ `/note-agent-actions?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
+ ),
+ ]);
+
+ 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);
return {
...toNoteSummary(note),
@@ -126,12 +229,13 @@ export async function getNoteDetail(noteId: string): Promise
metadata: {
owner: workspace?.members.find((member) => member.role === "owner")?.userId ?? note.updatedBy,
source: note.sourceType ?? "manual",
- reviewState: "none",
- taskCount: 0,
- artifactCount: 0,
+ reviewState: toReviewState(timeline[0]?.status),
+ taskCount: tasks.length,
+ artifactCount: artifacts.length,
},
linkedNotes: [],
- tasks: [],
- artifacts: [],
+ tasks,
+ artifacts,
+ timeline,
};
}
diff --git a/web/src/lib/review-client.ts b/web/src/lib/review-client.ts
new file mode 100644
index 0000000..c469de4
--- /dev/null
+++ b/web/src/lib/review-client.ts
@@ -0,0 +1,112 @@
+import { createApiClient } from "@bytelyst/api-client";
+import { NOTES_API_URL, PRODUCT_ID } from "@/lib/product-config";
+import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types";
+import { listWorkspaceSummaries } from "@/lib/notes-client";
+
+type NoteAgentActionDoc = {
+ id: string;
+ workspaceId: string;
+ noteId: string;
+ actorId: string;
+ actorType: "agent" | "human";
+ toolName: string;
+ actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation";
+ state: "draft" | "proposed" | "approved" | "rejected" | "applied";
+ reason?: string;
+ beforeSummary?: string;
+ afterSummary?: string;
+ updatedAt: string;
+};
+
+type NoteAgentActionListResponse = {
+ items: NoteAgentActionDoc[];
+};
+
+function getAccessToken(): string | null {
+ if (typeof window === "undefined") {
+ return null;
+ }
+
+ return localStorage.getItem(`${PRODUCT_ID}_access_token`);
+}
+
+function createNotesApiClient() {
+ return createApiClient({
+ baseUrl: NOTES_API_URL,
+ getToken: getAccessToken,
+ defaultHeaders: {
+ "x-product-id": PRODUCT_ID,
+ },
+ });
+}
+
+function toSeverity(actionType: NoteAgentActionDoc["actionType"]): ApprovalQueueItem["severity"] {
+ if (actionType === "update" || actionType === "extract_tasks") {
+ return "medium";
+ }
+
+ if (actionType === "attach_citation") {
+ return "low";
+ }
+
+ return "high";
+}
+
+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.toolName,
+ };
+}
+
+function toApprovalQueueItem(action: NoteAgentActionDoc): ApprovalQueueItem {
+ return {
+ id: action.id,
+ title: action.afterSummary ?? action.reason ?? `${action.actionType} proposal`,
+ owner: action.actorId,
+ severity: toSeverity(action.actionType),
+ status: action.state,
+ noteId: action.noteId,
+ workspaceId: action.workspaceId,
+ before: action.beforeSummary,
+ after: action.afterSummary,
+ };
+}
+
+async function listAgentActionsForWorkspace(workspaceId: string): Promise {
+ const api = createNotesApiClient();
+ const response = await api.fetch(
+ `/note-agent-actions?workspaceId=${encodeURIComponent(workspaceId)}`
+ );
+ return response.items;
+}
+
+export async function listApprovalQueue(): Promise {
+ const workspaces = await listWorkspaceSummaries();
+ const actionGroups = await Promise.all(
+ workspaces.map((workspace) => listAgentActionsForWorkspace(workspace.id))
+ );
+
+ return actionGroups
+ .flat()
+ .filter((action) => action.state === "draft" || action.state === "proposed")
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
+ .map(toApprovalQueueItem);
+}
+
+export async function listAgentTimeline(noteId?: string): Promise {
+ const workspaces = await listWorkspaceSummaries();
+ const actionGroups = await Promise.all(
+ workspaces.map((workspace) => listAgentActionsForWorkspace(workspace.id))
+ );
+
+ return actionGroups
+ .flat()
+ .filter((action) => (noteId ? action.noteId === noteId : true))
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
+ .map(toTimelineItem);
+}
diff --git a/web/src/lib/review-data.ts b/web/src/lib/review-data.ts
index 7a13932..97ccb1d 100644
--- a/web/src/lib/review-data.ts
+++ b/web/src/lib/review-data.ts
@@ -1,4 +1,4 @@
-import type { AgentTimelineItem } from "@/components/AgentTimeline";
+import type { AgentTimelineItem } from "@/lib/types";
export const mockAgentTimeline: AgentTimelineItem[] = [
{
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index 96cfde9..741f40a 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -70,6 +70,28 @@ export interface NoteDetail extends NoteSummary {
linkedNotes: LinkedNote[];
tasks: NoteTask[];
artifacts: ArtifactSummary[];
+ timeline: AgentTimelineItem[];
+}
+
+export interface AgentTimelineItem {
+ id: string;
+ actor: string;
+ action: string;
+ timestamp: string;
+ status: "draft" | "proposed" | "approved" | "rejected" | "applied";
+ summary: string;
+}
+
+export interface ApprovalQueueItem {
+ id: string;
+ title: string;
+ owner: string;
+ severity: "low" | "medium" | "high";
+ status: "draft" | "proposed" | "approved" | "rejected" | "applied";
+ noteId: string;
+ workspaceId: string;
+ before?: string;
+ after?: string;
}
export interface ProductUser {