feat(workstate): UI des résumés de conversation (Lot C frontend)
Affiche les résumés de conversation dans ProjectWorkStatePanel à partir du DTO camelCase : type de domaine et adaptateur mock alignés, panneau enrichi. Tests verts : workstate.test.tsx + projects.test.tsx (2 fichiers / 19), tsc --noEmit OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -3,7 +3,12 @@
|
||||
* and idle/busy state from the backend read-model, plus the current input queue.
|
||||
*/
|
||||
|
||||
import type { AgentTicketState, AgentWorkState } from "@/domain";
|
||||
import type {
|
||||
AgentTicketState,
|
||||
AgentWorkState,
|
||||
ConversationPreviewStatus,
|
||||
ConversationWorkSummary,
|
||||
} from "@/domain";
|
||||
import { Button, Panel, Spinner, cn } from "@/shared";
|
||||
import { useProjectWorkState } from "./useProjectWorkState";
|
||||
|
||||
@ -27,46 +32,105 @@ function requesterLabel(ticket: AgentTicketState): string {
|
||||
: `Agent ${id}`;
|
||||
}
|
||||
|
||||
function TicketRow({ ticket }: { ticket: AgentTicketState }) {
|
||||
const truncated = ticket.taskLen > ticket.taskPreview.length;
|
||||
function summaryLabel(status: ConversationPreviewStatus): string {
|
||||
switch (status) {
|
||||
case "ready":
|
||||
return "Summary";
|
||||
case "missing":
|
||||
return "No summary";
|
||||
case "partial":
|
||||
return "Partial";
|
||||
case "unavailable":
|
||||
return "Unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
function summaryText(summary: ConversationWorkSummary): string | null {
|
||||
if (summary.objectivePreview) return `Goal: ${summary.objectivePreview}`;
|
||||
if (summary.summaryPreview) return summary.summaryPreview;
|
||||
const lastTurn = summary.recentTurns.at(-1);
|
||||
return lastTurn ? `Last: ${lastTurn.textPreview}` : null;
|
||||
}
|
||||
|
||||
function ConversationSummaryLine({
|
||||
summary,
|
||||
}: {
|
||||
summary: ConversationWorkSummary;
|
||||
}) {
|
||||
const text = summaryText(summary);
|
||||
if (!text) return null;
|
||||
return (
|
||||
<li className="flex min-w-0 items-start gap-2 text-xs text-muted">
|
||||
<div className="mt-1 flex min-w-0 items-start gap-2 pl-1 text-xs text-muted">
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
|
||||
ticket.status === "inProgress"
|
||||
? "bg-warning/15 text-warning"
|
||||
summary.status === "ready"
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-raised text-muted",
|
||||
)}
|
||||
>
|
||||
#{ticket.position + 1} {formatStatus(ticket.status)}
|
||||
{summaryLabel(summary.status)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="font-medium text-content">{requesterLabel(ticket)}</span>
|
||||
<span className="text-muted"> · </span>
|
||||
<span className="break-words">{ticket.taskPreview}</span>
|
||||
{truncated && (
|
||||
<span
|
||||
aria-label={`${ticket.taskLen - ticket.taskPreview.length} more characters`}
|
||||
className="text-muted"
|
||||
>
|
||||
{" "}
|
||||
+{ticket.taskLen - ticket.taskPreview.length}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<code
|
||||
aria-label={`ticket ${shortTicket(ticket.ticketId)}`}
|
||||
title={ticket.ticketId}
|
||||
className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
|
||||
>
|
||||
{shortTicket(ticket.ticketId)}
|
||||
</code>
|
||||
<span className="min-w-0 break-words">{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TicketRow({
|
||||
ticket,
|
||||
summary,
|
||||
}: {
|
||||
ticket: AgentTicketState;
|
||||
summary?: ConversationWorkSummary;
|
||||
}) {
|
||||
const truncated = ticket.taskLen > ticket.taskPreview.length;
|
||||
return (
|
||||
<li className="min-w-0 text-xs text-muted">
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
|
||||
ticket.status === "inProgress"
|
||||
? "bg-warning/15 text-warning"
|
||||
: "bg-raised text-muted",
|
||||
)}
|
||||
>
|
||||
#{ticket.position + 1} {formatStatus(ticket.status)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="font-medium text-content">{requesterLabel(ticket)}</span>
|
||||
<span className="text-muted"> · </span>
|
||||
<span className="break-words">{ticket.taskPreview}</span>
|
||||
{truncated && (
|
||||
<span
|
||||
aria-label={`${ticket.taskLen - ticket.taskPreview.length} more characters`}
|
||||
className="text-muted"
|
||||
>
|
||||
{" "}
|
||||
+{ticket.taskLen - ticket.taskPreview.length}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<code
|
||||
aria-label={`ticket ${shortTicket(ticket.ticketId)}`}
|
||||
title={ticket.ticketId}
|
||||
className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
|
||||
>
|
||||
{shortTicket(ticket.ticketId)}
|
||||
</code>
|
||||
</div>
|
||||
{summary && <ConversationSummaryLine summary={summary} />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentRow({ agent }: { agent: AgentWorkState }) {
|
||||
function AgentRow({
|
||||
agent,
|
||||
conversations,
|
||||
}: {
|
||||
agent: AgentWorkState;
|
||||
conversations: Map<string, ConversationWorkSummary>;
|
||||
}) {
|
||||
const live = agent.live !== undefined;
|
||||
const busy = agent.busy.state === "busy";
|
||||
const tickets = [...agent.tickets].sort((a, b) => a.position - b.position);
|
||||
@ -117,7 +181,11 @@ function AgentRow({ agent }: { agent: AgentWorkState }) {
|
||||
className="mt-2 flex flex-col gap-1"
|
||||
>
|
||||
{tickets.map((ticket) => (
|
||||
<TicketRow key={ticket.ticketId} ticket={ticket} />
|
||||
<TicketRow
|
||||
key={ticket.ticketId}
|
||||
ticket={ticket}
|
||||
summary={conversations.get(ticket.conversationId)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
@ -128,6 +196,12 @@ function AgentRow({ agent }: { agent: AgentWorkState }) {
|
||||
export function ProjectWorkStatePanel({ projectId }: ProjectWorkStatePanelProps) {
|
||||
const vm = useProjectWorkState(projectId);
|
||||
const agents = vm.state?.agents ?? [];
|
||||
const conversations = new Map(
|
||||
(vm.state?.conversations ?? []).map((summary) => [
|
||||
summary.conversationId,
|
||||
summary,
|
||||
]),
|
||||
);
|
||||
|
||||
return (
|
||||
<Panel
|
||||
@ -161,7 +235,11 @@ export function ProjectWorkStatePanel({ projectId }: ProjectWorkStatePanelProps)
|
||||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{agents.map((agent) => (
|
||||
<AgentRow key={agent.agentId} agent={agent} />
|
||||
<AgentRow
|
||||
key={agent.agentId}
|
||||
agent={agent}
|
||||
conversations={conversations}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user