@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
// The project's runs, live (umbrella TS6.4 and TS6.6): the page's view renders the first page and hands it
|
||||
// over as the seed, so the page arrives complete; this keeps it current through the organization's live
|
||||
// stream -- a run another member starts appears, and each run's status and progress move as its steps
|
||||
// commit, without a reload. The rows come from `agent.run.getMany`, under the same permission as the list,
|
||||
// so the stream itself never carries a run's goal or plan.
|
||||
|
||||
import type { ProjectSlugs } from "@conusai-kit/ui"
|
||||
import { liveCollection, useDataClient, useLive } from "@conusai-kit/ui/data"
|
||||
import { PageSection, type ResourceColumn, ResourceList } from "@conusai-kit/ui/patterns"
|
||||
import { type ReactElement, useState } from "react"
|
||||
import { RUN_COLLECTION, type RunView } from "../contract/index.js"
|
||||
|
||||
export interface RunListLabels {
|
||||
readonly heading: string
|
||||
/** What to say when there is no run. */
|
||||
readonly empty: string
|
||||
readonly goal: string
|
||||
readonly status: string
|
||||
readonly progress: string
|
||||
readonly startedBy: string
|
||||
/** The message of each run status, by status. */
|
||||
readonly statuses: Record<RunView["status"], string>
|
||||
}
|
||||
|
||||
export function useLiveRuns(slugs: ProjectSlugs, seed: readonly RunView[]): readonly RunView[] {
|
||||
const client = useDataClient()
|
||||
// One collection for the life of the page; the seed is the first render's.
|
||||
const [runs] = useState(() =>
|
||||
liveCollection<RunView>({
|
||||
client,
|
||||
org: slugs.org,
|
||||
topic: { ws: slugs.workspace, project: slugs.project },
|
||||
entity: RUN_COLLECTION.entity,
|
||||
key: RUN_COLLECTION.key,
|
||||
seed,
|
||||
list: {
|
||||
procedure: RUN_COLLECTION.list,
|
||||
input: { ...slugs },
|
||||
rows: (out) => (out as { runs: RunView[] }).runs,
|
||||
},
|
||||
getMany: {
|
||||
procedure: RUN_COLLECTION.getMany,
|
||||
input: { ...slugs },
|
||||
rows: (out) => (out as { items: RunView[] }).items,
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Newest first, as the list procedure orders them.
|
||||
const { data, isReady } = useLive(runs, (q, rows) =>
|
||||
q.from({ run: rows }).orderBy(({ run }: { run: RunView }) => run.createdAt, "desc"),
|
||||
)
|
||||
// Until the collection is ready the server's rows are the answer, so the first paint matches it.
|
||||
return isReady ? data : seed
|
||||
}
|
||||
|
||||
export function RunList({
|
||||
slugs,
|
||||
seed,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
seed: readonly RunView[]
|
||||
labels: RunListLabels
|
||||
}): ReactElement {
|
||||
const rows = useLiveRuns(slugs, seed)
|
||||
const pageHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/agent`
|
||||
const columns: ResourceColumn<RunView>[] = [
|
||||
{ id: "goal", header: labels.goal, wrap: true },
|
||||
{ id: "status", header: labels.status, cell: (run) => labels.statuses[run.status] },
|
||||
{
|
||||
id: "percent",
|
||||
header: labels.progress,
|
||||
cell: (run) => (
|
||||
<progress value={run.percent} max={1}>
|
||||
{Math.round(run.percent * 100)}%
|
||||
</progress>
|
||||
),
|
||||
},
|
||||
{ id: "actorId", header: labels.startedBy },
|
||||
]
|
||||
return (
|
||||
<PageSection title={labels.heading}>
|
||||
<ResourceList
|
||||
label={labels.heading}
|
||||
columns={columns}
|
||||
data={rows}
|
||||
rowId={(run) => run.runId}
|
||||
rowHref={(run) => `${pageHref}/${run.runId}`}
|
||||
rowLabel={(run) => run.goal}
|
||||
empty={{ title: labels.empty }}
|
||||
/>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user