add deliberation timeline UI

This commit is contained in:
2025-05-04 16:14:57 +00:00
parent 2041d23751
commit 43161635d0
2 changed files with 176 additions and 0 deletions

View File

@@ -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 (
<div className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between gap-4 mb-3">
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg} ${config.text}`}
>
{config.label}
</span>
<span className="text-xs text-gray-500">
Weight: {conflict.severity_weight.toFixed(2)}
</span>
</div>
<p className="text-sm text-gray-700 mb-3">{conflict.description}</p>
{conflict.resolution && (
<div className="bg-green-50 border border-green-200 rounded-md p-3">
<h4 className="text-xs font-medium text-green-800 uppercase tracking-wide mb-1">
Resolution
</h4>
<p className="text-sm text-green-700">{conflict.resolution}</p>
</div>
)}
<div className="mt-2 text-xs text-gray-500">
Findings involved: {conflict.finding_ids.join(', ')}
</div>
</div>
);
}

View File

@@ -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 (
<div className="relative">
{!isLast && (
<div className="absolute left-5 top-10 bottom-0 w-0.5 bg-gray-200" />
)}
<div className="flex gap-4">
<div
className={`flex-shrink-0 w-10 h-10 rounded-full ${config.bg} flex items-center justify-center`}
>
<span>{config.icon}</span>
</div>
<div className="flex-1 pb-6">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full text-left"
>
<div className="flex items-center justify-between">
<span className={`text-sm font-medium ${config.color}`}>
{config.label}
</span>
<span className="text-xs text-gray-500">
{formatTime(step.timestamp)}
</span>
</div>
<p className="text-sm text-gray-700 mt-1">{step.description}</p>
</button>
{isExpanded && Object.keys(step.details).length > 0 && (
<div className="mt-3 bg-gray-50 rounded-md p-3">
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">
Details
</h4>
<pre className="text-xs text-gray-700 overflow-x-auto">
{JSON.stringify(step.details, null, 2)}
</pre>
</div>
)}
</div>
</div>
</div>
);
}
export function DeliberationTimeline({ steps }: DeliberationTimelineProps) {
const sortedSteps = [...steps].sort((a, b) => a.sequence - b.sequence);
if (steps.length === 0) {
return (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-6 text-center">
<p className="text-gray-600">No deliberation steps recorded</p>
</div>
);
}
return (
<div className="bg-white border border-gray-200 rounded-lg p-4">
{sortedSteps.map((step, index) => (
<StepItem
key={step.id}
step={step}
isLast={index === sortedSteps.length - 1}
/>
))}
</div>
);
}