diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx index 74fc2d7d..3c2383a8 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx @@ -6,10 +6,8 @@ const meta: Meta = { title: "Groups/TaskCard", component: TaskCard, args: { - onViewDetails: fn(), - onViewResult: fn(), onCancel: fn(), - onDismiss: fn(), + isListExpanded: true, }, }; @@ -18,77 +16,82 @@ type Story = StoryObj; export const Running: Story = { args: { - taskId: "task-abc-123", + taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", status: "running", method: "tools/call", target: "generate_report", progress: 80, - progressDescription: "Generating report section 4 of 5...", - startedAt: "2026-03-17T10:30:00Z", - elapsed: "2m 15s", - }, -}; - -export const RunningWithProgress: Story = { - args: { - taskId: "task-def-456", - status: "running", - method: "tools/call", - target: "analyze_data", - progress: 45, - progressDescription: "Analyzing dataset...", - startedAt: "2026-03-17T10:32:00Z", - elapsed: "30s", + progressDescription: "Synthesizing findings...", + startedAt: "3/29/2026, 8:18:20 PM", + lastUpdated: "3/29/2026, 8:18:22 PM", + ttl: 300000, }, }; export const Waiting: Story = { args: { - taskId: "task-ghi-789", + taskId: "4100b5e0b0ed9cd0023330342d1bf647", status: "waiting", method: "resources/read", target: "file:///data/report.csv", - progress: 0, - startedAt: "2026-03-17T10:35:00Z", + startedAt: "3/29/2026, 8:17:55 PM", + lastUpdated: "3/29/2026, 8:17:55 PM", + ttl: 300000, }, }; export const Completed: Story = { args: { - taskId: "task-jkl-012", + taskId: "d487b49aa39023d907b5a2a5b506cb3", status: "completed", method: "tools/call", target: "generate_summary", - progress: 100, - startedAt: "2026-03-17T10:20:00Z", - completedAt: "2026-03-17T10:25:00Z", - elapsed: "5m 00s", + startedAt: "3/29/2026, 8:16:47 PM", + completedAt: "3/29/2026, 8:16:51 PM", + lastUpdated: "3/29/2026, 8:16:51 PM", + ttl: 300000, + progressDescription: "Generating report...", }, }; export const Failed: Story = { args: { - taskId: "task-mno-345", + taskId: "e6ebffd9cca84ddd1646d3c579a4d453", status: "failed", method: "tools/call", target: "fetch_remote_data", - progress: 45, - startedAt: "2026-03-17T10:28:00Z", - completedAt: "2026-03-17T10:29:30Z", - elapsed: "1m 30s", + startedAt: "3/29/2026, 8:17:27 PM", + completedAt: "3/29/2026, 8:17:28 PM", + lastUpdated: "3/29/2026, 8:17:28 PM", + ttl: 300000, error: "Connection refused: upstream server not responding after 3 retries", }, }; export const Cancelled: Story = { args: { - taskId: "task-pqr-678", + taskId: "c416bc183cb04468d3f81696f1b868f6", status: "cancelled", method: "tools/call", target: "long_running_analysis", - progress: 30, - startedAt: "2026-03-17T10:15:00Z", - completedAt: "2026-03-17T10:18:00Z", - elapsed: "3m 00s", + startedAt: "3/29/2026, 8:18:15 PM", + completedAt: "3/29/2026, 8:18:18 PM", + lastUpdated: "3/29/2026, 8:18:18 PM", + ttl: 300000, + }, +}; + +export const Collapsed: Story = { + args: { + taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", + status: "running", + method: "tools/call", + target: "generate_report", + progress: 80, + progressDescription: "Synthesizing findings...", + startedAt: "3/29/2026, 8:18:20 PM", + lastUpdated: "3/29/2026, 8:18:22 PM", + ttl: 300000, + isListExpanded: false, }, }; diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.tsx index bea68237..f9b0e909 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.tsx @@ -1,4 +1,15 @@ -import { Badge, Button, Card, Group, Stack, Text } from "@mantine/core"; +import { useEffect, useState } from "react"; +import { + Badge, + Button, + Card, + Collapse, + Divider, + Group, + Stack, + Text, +} from "@mantine/core"; +import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay"; export type TaskStatus = @@ -17,12 +28,12 @@ export interface TaskCardProps { progressDescription?: string; startedAt?: string; completedAt?: string; + lastUpdated?: string; elapsed?: string; + ttl?: number; error?: string; - onViewDetails: () => void; - onViewResult: () => void; + isListExpanded: boolean; onCancel: () => void; - onDismiss: () => void; } const STATUS_COLORS: Record = { @@ -38,27 +49,37 @@ const TaskContainer = Card.withProps({ padding: "md", }); +const HeaderRow = Group.withProps({ + justify: "space-between", + wrap: "nowrap", +}); + const TaskIdText = Text.withProps({ size: "sm", ff: "monospace", + c: "dimmed", }); -const DetailText = Text.withProps({ +const DetailLabel = Text.withProps({ size: "sm", c: "dimmed", }); -const ValueSpan = Text.withProps({ - component: "span", - fw: 600, +const DetailValue = Text.withProps({ + size: "sm", + fw: 500, }); -const ProgressNote = Text.withProps({ +const StatusMessageText = Text.withProps({ size: "sm", - c: "dimmed", fs: "italic", }); +const ErrorText = Text.withProps({ + size: "sm", + c: "red", +}); + const SubtleButton = Button.withProps({ variant: "subtle", size: "xs", @@ -70,89 +91,152 @@ const CancelButton = Button.withProps({ size: "xs", }); +const DetailRow = Group.withProps({ + gap: "xl", + wrap: "wrap", +}); + +const SummaryRow = Group.withProps({ + justify: "space-between", + wrap: "nowrap", +}); + +const HeaderLeftGroup = Group.withProps({ + gap: "sm", +}); + +const HeaderRightGroup = Group.withProps({ + gap: "xs", +}); + +const SectionTitle = Text.withProps({ + size: "sm", + fw: 600, +}); + function formatTaskId(taskId: string): string { - return `Task: ${taskId}`; + return `ID: ${taskId}`; } -function formatTimeline( - startedAt: string, - completedAt?: string, - elapsed?: string, -): string { - let result = `Started: ${startedAt}`; - if (completedAt) result += ` | Completed: ${completedAt}`; - if (elapsed) result += ` | Elapsed: ${elapsed}`; - return result; +function formatTtl(ttl: number): string { + return `${ttl}ms`; } -export function TaskCard({ - taskId, - status, - method, - target, - progress, - progressDescription, - startedAt, - completedAt, - elapsed, - error, - onViewDetails, - onViewResult, - onCancel, - onDismiss, -}: TaskCardProps) { +function buildTaskObject(props: TaskCardProps): string { + const obj: Record = { + taskId: props.taskId, + status: props.status, + method: props.method, + }; + if (props.target) obj.target = props.target; + if (props.ttl !== undefined) obj.ttl = props.ttl; + if (props.startedAt) obj.createdAt = props.startedAt; + if (props.lastUpdated) obj.lastUpdatedAt = props.lastUpdated; + if (props.completedAt) obj.completedAt = props.completedAt; + if (props.progress !== undefined) obj.progress = props.progress; + if (props.progressDescription) obj.statusMessage = props.progressDescription; + if (props.error) obj.error = props.error; + return JSON.stringify(obj); +} + +export function TaskCard(props: TaskCardProps) { + const { + taskId, + status, + progress, + progressDescription, + startedAt, + lastUpdated, + ttl, + error, + isListExpanded, + onCancel, + } = props; + + const [isExpanded, setIsExpanded] = useState(isListExpanded); const isActive = status === "waiting" || status === "running"; + useEffect(() => { + setIsExpanded(isListExpanded); + }, [isListExpanded]); + return ( - - - + + + + Task Details {formatTaskId(taskId)} + + + {isActive && ( + Cancel Task + )} {status} - - {progress !== undefined && } - + + - - Method: {method} - + + + + Status + {status} + + {lastUpdated && ( + + Last Updated + {lastUpdated} + + )} + {startedAt && ( + + Created At + {startedAt} + + )} + {ttl !== undefined && ( + + TTL + {formatTtl(ttl)} + + )} + + setIsExpanded((v) => !v)}> + {isExpanded ? "Collapse" : "Expand"} + + - {target && ( - - Tool/Resource: {target} - + {isExpanded && ( + + + {progressDescription && ( + + Status Message + {progressDescription} + + )} + + {progress !== undefined && ( + + )} + + {error && ( + + Error + {error} + + )} + + + Full Task Object + + + )} - - {startedAt && ( - - {formatTimeline(startedAt, completedAt, elapsed)} - - )} - - {progressDescription && ( - {progressDescription} - )} - - {error && ( - - {error} - - )} - - - {isActive ? ( - <> - View Details - Cancel - - ) : ( - <> - View Result - Dismiss - - )} - ); diff --git a/clients/web/src/components/groups/TaskControls/TaskControls.stories.tsx b/clients/web/src/components/groups/TaskControls/TaskControls.stories.tsx new file mode 100644 index 00000000..87a35459 --- /dev/null +++ b/clients/web/src/components/groups/TaskControls/TaskControls.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { TaskControls } from "./TaskControls"; + +const meta: Meta = { + title: "Groups/TaskControls", + component: TaskControls, + args: { + searchText: "", + onSearchChange: fn(), + onStatusFilterChange: fn(), + onRefresh: fn(), + onClearHistory: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithSearch: Story = { + args: { + searchText: "generate", + }, +}; + +export const WithFilter: Story = { + args: { + statusFilter: "running", + }, +}; diff --git a/clients/web/src/components/groups/TaskControls/TaskControls.tsx b/clients/web/src/components/groups/TaskControls/TaskControls.tsx new file mode 100644 index 00000000..22fd091b --- /dev/null +++ b/clients/web/src/components/groups/TaskControls/TaskControls.tsx @@ -0,0 +1,60 @@ +import { Button, Group, Select, Stack, TextInput, Title } from "@mantine/core"; +import type { TaskStatus } from "../TaskCard/TaskCard"; + +const STATUS_OPTIONS: TaskStatus[] = [ + "waiting", + "running", + "completed", + "failed", + "cancelled", +]; + +const ToolbarButton = Button.withProps({ + variant: "light", + size: "sm", +}); + +export interface TaskControlsProps { + searchText: string; + statusFilter?: string; + onSearchChange: (text: string) => void; + onStatusFilterChange: (status: string) => void; + onRefresh: () => void; + onClearHistory: () => void; +} + +export function TaskControls({ + searchText, + statusFilter, + onSearchChange, + onStatusFilterChange, + onRefresh, + onClearHistory, +}: TaskControlsProps) { + return ( + + Tasks + + Search + onSearchChange(event.currentTarget.value)} + /> + + Filter by Status +