summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/history/TimelineEventCard.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/history/TimelineEventCard.tsx')
-rw-r--r--makima/frontend/src/components/history/TimelineEventCard.tsx139
1 files changed, 139 insertions, 0 deletions
diff --git a/makima/frontend/src/components/history/TimelineEventCard.tsx b/makima/frontend/src/components/history/TimelineEventCard.tsx
new file mode 100644
index 0000000..f48466f
--- /dev/null
+++ b/makima/frontend/src/components/history/TimelineEventCard.tsx
@@ -0,0 +1,139 @@
+import type { HistoryEvent } from "../../lib/api";
+
+interface TimelineEventCardProps {
+ event: HistoryEvent;
+ isSelected: boolean;
+ onClick: () => void;
+}
+
+// Get icon and color based on event type
+function getEventStyle(eventType: string, eventSubtype: string | null) {
+ const type = eventType.toLowerCase();
+ const subtype = eventSubtype?.toLowerCase();
+
+ if (type === "task") {
+ if (subtype === "created") return { icon: "+", color: "text-[#9bc3ff]" };
+ if (subtype === "started") return { icon: "\u25B6", color: "text-green-400" };
+ if (subtype === "completed") return { icon: "\u2713", color: "text-emerald-400" };
+ if (subtype === "failed") return { icon: "\u2717", color: "text-red-400" };
+ if (subtype === "stopped") return { icon: "\u25A0", color: "text-yellow-400" };
+ return { icon: "\u2022", color: "text-[#9bc3ff]" };
+ }
+
+ if (type === "checkpoint") {
+ return { icon: "\u2691", color: "text-purple-400" };
+ }
+
+ if (type === "phase") {
+ return { icon: "\u21B3", color: "text-cyan-400" };
+ }
+
+ if (type === "chat") {
+ return { icon: "\u2709", color: "text-[#75aafc]" };
+ }
+
+ if (type === "contract") {
+ return { icon: "\u2606", color: "text-[#9bc3ff]" };
+ }
+
+ if (type === "file") {
+ return { icon: "\u2630", color: "text-[#7788aa]" };
+ }
+
+ return { icon: "\u2022", color: "text-[#7788aa]" };
+}
+
+// Format relative time
+function formatRelativeTime(dateStr: string): string {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffSec = Math.floor(diffMs / 1000);
+ const diffMin = Math.floor(diffSec / 60);
+ const diffHour = Math.floor(diffMin / 60);
+ const diffDay = Math.floor(diffHour / 24);
+
+ if (diffSec < 60) return "just now";
+ if (diffMin < 60) return `${diffMin}m ago`;
+ if (diffHour < 24) return `${diffHour}h ago`;
+ if (diffDay < 7) return `${diffDay}d ago`;
+
+ return date.toLocaleDateString();
+}
+
+// Extract a preview from event data
+function getEventPreview(event: HistoryEvent): string {
+ const data = event.eventData as Record<string, unknown>;
+
+ // Task events
+ if (data.taskName) return String(data.taskName);
+ if (data.name) return String(data.name);
+
+ // Chat events
+ if (data.message) {
+ const msg = String(data.message);
+ return msg.length > 50 ? msg.slice(0, 50) + "..." : msg;
+ }
+
+ // Checkpoint events
+ if (data.checkpointMessage) return String(data.checkpointMessage);
+ if (data.commitSha) return `Commit ${String(data.commitSha).slice(0, 7)}`;
+
+ // Phase events
+ if (data.phase) return `Phase: ${data.phase}`;
+
+ // Contract events
+ if (data.contractName) return String(data.contractName);
+
+ return "";
+}
+
+export function TimelineEventCard({ event, isSelected, onClick }: TimelineEventCardProps) {
+ const { icon, color } = getEventStyle(event.eventType, event.eventSubtype);
+ const preview = getEventPreview(event);
+
+ return (
+ <button
+ onClick={onClick}
+ className={`w-full text-left p-3 transition-colors ${
+ isSelected
+ ? "bg-[rgba(63,111,179,0.2)] border-l-2 border-[#3f6fb3]"
+ : "hover:bg-[rgba(117,170,252,0.05)] border-l-2 border-transparent"
+ }`}
+ >
+ <div className="flex items-start gap-3">
+ {/* Icon */}
+ <div className={`font-mono text-sm ${color}`}>{icon}</div>
+
+ {/* Content */}
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center justify-between gap-2">
+ <div className="font-mono text-xs text-[#9bc3ff] uppercase truncate">
+ {event.eventType}
+ {event.eventSubtype && (
+ <span className="text-[#7788aa]"> / {event.eventSubtype}</span>
+ )}
+ </div>
+ <div className="font-mono text-[10px] text-[#556677] shrink-0">
+ {formatRelativeTime(event.createdAt)}
+ </div>
+ </div>
+
+ {preview && (
+ <div className="font-mono text-[10px] text-[#7788aa] mt-1 truncate">
+ {preview}
+ </div>
+ )}
+
+ {event.phase && (
+ <div className="mt-1">
+ <span className="font-mono text-[9px] text-[#75aafc] uppercase px-1.5 py-0.5 border border-[rgba(117,170,252,0.25)]">
+ {event.phase}
+ </span>
+ </div>
+ )}
+ </div>
+ </div>
+ </button>
+ );
+}