From 43161635d002de5d6ca74e327b841298ee966c82 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sun, 4 May 2025 16:14:57 +0000 Subject: [PATCH] add deliberation timeline UI --- .../components/deliberation/ConflictCard.tsx | 57 +++++++++ .../deliberation/DeliberationTimeline.tsx | 119 ++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 dashboard/src/components/deliberation/ConflictCard.tsx create mode 100644 dashboard/src/components/deliberation/DeliberationTimeline.tsx diff --git a/dashboard/src/components/deliberation/ConflictCard.tsx b/dashboard/src/components/deliberation/ConflictCard.tsx new file mode 100644 index 0000000..b4f8922 --- /dev/null +++ b/dashboard/src/components/deliberation/ConflictCard.tsx @@ -0,0 +1,57 @@ +import type { Conflict, ConflictNature } from '../../types/api'; + +interface ConflictCardProps { + conflict: Conflict; +} + +const natureConfig: Record< + ConflictNature, + { bg: string; text: string; label: string } +> = { + contradictory: { + bg: 'bg-red-100', + text: 'text-red-800', + label: 'Contradictory', + }, + trade_off: { + bg: 'bg-yellow-100', + text: 'text-yellow-800', + label: 'Trade-off', + }, + overlapping: { + bg: 'bg-blue-100', + text: 'text-blue-800', + label: 'Overlapping', + }, +}; + +export function ConflictCard({ conflict }: ConflictCardProps) { + const config = natureConfig[conflict.nature]; + + return ( +
+
+ + {config.label} + + + Weight: {conflict.severity_weight.toFixed(2)} + +
+

{conflict.description}

+ {conflict.resolution && ( +
+

+ Resolution +

+

{conflict.resolution}

+
+ )} +
+ Findings involved: {conflict.finding_ids.join(', ')} +
+
+ ); +} diff --git a/dashboard/src/components/deliberation/DeliberationTimeline.tsx b/dashboard/src/components/deliberation/DeliberationTimeline.tsx new file mode 100644 index 0000000..e387169 --- /dev/null +++ b/dashboard/src/components/deliberation/DeliberationTimeline.tsx @@ -0,0 +1,119 @@ +import { useState } from 'react'; +import type { DeliberationStep, StepType } from '../../types/api'; + +interface DeliberationTimelineProps { + steps: DeliberationStep[]; +} + +const stepConfig: Record< + StepType, + { icon: string; color: string; bg: string; label: string } +> = { + merge: { + icon: '๐Ÿ”€', + color: 'text-blue-600', + bg: 'bg-blue-100', + label: 'Merge', + }, + conflict_detection: { + icon: 'โš ๏ธ', + color: 'text-yellow-600', + bg: 'bg-yellow-100', + label: 'Conflict Detection', + }, + synthesis: { + icon: '๐Ÿงช', + color: 'text-purple-600', + bg: 'bg-purple-100', + label: 'Synthesis', + }, + verdict: { + icon: 'โš–๏ธ', + color: 'text-green-600', + bg: 'bg-green-100', + label: 'Verdict', + }, +}; + +function formatTime(timestamp: string): string { + return new Date(timestamp).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); +} + +interface StepItemProps { + step: DeliberationStep; + isLast: boolean; +} + +function StepItem({ step, isLast }: StepItemProps) { + const [isExpanded, setIsExpanded] = useState(false); + const config = stepConfig[step.step_type]; + + return ( +
+ {!isLast && ( +
+ )} +
+
+ {config.icon} +
+
+ + {isExpanded && Object.keys(step.details).length > 0 && ( +
+

+ Details +

+
+                {JSON.stringify(step.details, null, 2)}
+              
+
+ )} +
+
+
+ ); +} + +export function DeliberationTimeline({ steps }: DeliberationTimelineProps) { + const sortedSteps = [...steps].sort((a, b) => a.sequence - b.sequence); + + if (steps.length === 0) { + return ( +
+

No deliberation steps recorded

+
+ ); + } + + return ( +
+ {sortedSteps.map((step, index) => ( + + ))} +
+ ); +}