add deliberation timeline UI
This commit is contained in:
57
dashboard/src/components/deliberation/ConflictCard.tsx
Normal file
57
dashboard/src/components/deliberation/ConflictCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
dashboard/src/components/deliberation/DeliberationTimeline.tsx
Normal file
119
dashboard/src/components/deliberation/DeliberationTimeline.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user