From 8bf0bb54525467a8e244c9f10b070bde5b8fd2dd Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 10 Mar 2026 12:35:16 -0700 Subject: [PATCH] feat(web): back review surfaces with backend data --- web/src/app/(app)/notes/[noteId]/page.tsx | 3 +- web/src/app/(app)/reviews/page.tsx | 44 ++++++-- web/src/components/AgentTimeline.tsx | 9 +- web/src/lib/mock-data.ts | 30 ++++++ web/src/lib/notes-client.ts | 116 ++++++++++++++++++++-- web/src/lib/review-client.ts | 112 +++++++++++++++++++++ web/src/lib/review-data.ts | 2 +- web/src/lib/types.ts | 22 ++++ 8 files changed, 313 insertions(+), 25 deletions(-) create mode 100644 web/src/lib/review-client.ts diff --git a/web/src/app/(app)/notes/[noteId]/page.tsx b/web/src/app/(app)/notes/[noteId]/page.tsx index 61569f3..425ce13 100644 --- a/web/src/app/(app)/notes/[noteId]/page.tsx +++ b/web/src/app/(app)/notes/[noteId]/page.tsx @@ -10,7 +10,6 @@ import { TaskReviewPanel } from "@/components/TaskReviewPanel"; import { ArtifactPanel } from "@/components/ArtifactPanel"; import { AgentTimeline } from "@/components/AgentTimeline"; import { getNoteDetail } from "@/lib/notes-client"; -import { mockAgentTimeline } from "@/lib/review-data"; import type { NoteDetail } from "@/lib/types"; export default function NoteDetailPage() { @@ -57,7 +56,7 @@ export default function NoteDetailPage() { - + diff --git a/web/src/app/(app)/reviews/page.tsx b/web/src/app/(app)/reviews/page.tsx index f999ad7..338bf1b 100644 --- a/web/src/app/(app)/reviews/page.tsx +++ b/web/src/app/(app)/reviews/page.tsx @@ -1,10 +1,35 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/AppShell"; import { AgentTimeline } from "@/components/AgentTimeline"; import { ProposalReviewCard } from "@/components/ProposalReviewCard"; import { mockOperatorWorkflows } from "@/lib/mock-data"; -import { mockAgentTimeline, mockApprovalQueue } from "@/lib/review-data"; +import { listAgentTimeline, listApprovalQueue } from "@/lib/review-client"; +import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types"; export default function ReviewsPage() { + const [approvalQueue, setApprovalQueue] = useState([]); + const [timeline, setTimeline] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + void (async () => { + try { + const [nextQueue, nextTimeline] = await Promise.all([ + listApprovalQueue(), + listAgentTimeline(), + ]); + setApprovalQueue(nextQueue); + setTimeline(nextTimeline); + } catch (err) { + setError(err instanceof Error ? err.message : "Unable to load review queue"); + } + })(); + }, []); + + const featuredProposal = useMemo(() => approvalQueue[0] ?? null, [approvalQueue]); + return ( owner:any + {error ?
{error}
: null}
- {mockApprovalQueue.map((item) => ( + {approvalQueue.map((item) => (
{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 {