diff options
| author | soryu <soryu@soryu.co> | 2026-01-16 12:23:49 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-16 12:23:49 +0000 |
| commit | 205ab8a223ddf6591a3e8bfc9108506502977c11 (patch) | |
| tree | d768063acff233dbeea223d7b6ea69d7e3038300 /makima/frontend/src/components/history/TimelineEventCard.tsx | |
| parent | 05931d19bc0c161d0177c3f983d0cd903d5e8ae3 (diff) | |
| download | soryu-205ab8a223ddf6591a3e8bfc9108506502977c11.tar.gz soryu-205ab8a223ddf6591a3e8bfc9108506502977c11.zip | |
Fixup: use default api.makima.jp URL and fix default branch detection
Also add checkpointing/history
Diffstat (limited to 'makima/frontend/src/components/history/TimelineEventCard.tsx')
| -rw-r--r-- | makima/frontend/src/components/history/TimelineEventCard.tsx | 139 |
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> + ); +} |
