import { t } from "@lingui/core/macro"; import { Trans, useLingui } from "@lingui/react/macro"; import type { ProjectGoal, ProjectGoalStatus, ProjectSession } from "@rakazo/contracts"; import { useCallback, useEffect, useState } from "react"; import { rpc } from "../lib/rpc"; const INITIAL_VISIBLE = 4; function goalStatusTone(status: ProjectGoalStatus): string { if (status === "done") return "text-success"; if (status === "doing") return "text-warning"; return "text-foreground"; } function goalStatusLabel(status: ProjectGoalStatus): string { switch (status) { case "open": return t`To do`; case "doing": return t`In progress`; case "done": return t`Done`; default: { const _exhaustive: never = status; return _exhaustive; } } } function sessionStatusTone(status: ProjectSession["status"]): string { if (status === "failed") return "text-destructive"; if (status === "cancelled") return "text-muted-foreground"; if (status === "completed") return "text-success"; if (status === "waiting_input" || status === "waiting_takeover") return "text-warning"; return "text-foreground"; } function sessionStatusLabel(status: ProjectSession["status"]): string { switch (status) { case "queued": return t`Queued`; case "leased": case "running": return t`Running`; case "waiting_input": return t`Waiting`; case "waiting_takeover": return t`Takeover`; case "completed": return t`Done`; case "failed": return t`Failed`; case "cancelled": return t`Cancelled`; default: { const _exhaustive: never = status; return _exhaustive; } } } function formatRelativeTime(iso: string): string { const delta = Date.now() - new Date(iso).getTime(); const minutes = Math.round(delta / 60_000); if (minutes < 1) return t`just now`; if (minutes < 60) return t`${minutes}m ago`; const hours = Math.round(minutes / 60); if (hours < 24) return t`${hours}h ago`; const days = Math.round(hours / 24); return t`${days}d ago`; } type ProjectSessionTimelineProps = { sectionId: string; sectionName: string; groupId: string; groupName: string; onOpenSession: (sourceMessageId: string) => void; onStartSession: () => void; onOpenGroupChat: (groupId: string) => void; onStartVoiceFirst: (groupId: string) => void; }; export function ProjectSessionTimeline({ sectionId, sectionName, groupId, groupName, onOpenSession, onStartSession, onOpenGroupChat, onStartVoiceFirst, }: ProjectSessionTimelineProps) { const { t } = useLingui(); const [goals, setGoals] = useState([]); const [sessions, setSessions] = useState([]); const [loadingGoals, setLoadingGoals] = useState(true); const [loadingSessions, setLoadingSessions] = useState(true); const [sessionsExpanded, setSessionsExpanded] = useState(false); const [creatingGoal, setCreatingGoal] = useState(false); const [newGoalTitle, setNewGoalTitle] = useState(""); const reloadGoals = useCallback(async () => { const result = await rpc.projectGoals.list({ sectionId, groupId, seedDefaults: true, }); setGoals(result); }, [groupId, sectionId]); useEffect(() => { let cancelled = false; void reloadGoals() .catch(() => { if (!cancelled) setGoals([]); }) .finally(() => { if (!cancelled) setLoadingGoals(false); }); return () => { cancelled = true; }; }, [reloadGoals]); useEffect(() => { let cancelled = false; void rpc.groups .listSessions({ groupId }) .then((result) => { if (!cancelled) setSessions(result.sessions); }) .catch(() => { if (!cancelled) setSessions([]); }) .finally(() => { if (!cancelled) setLoadingSessions(false); }); return () => { cancelled = true; }; }, [groupId]); const visibleSessions = sessionsExpanded ? sessions : sessions.slice(0, INITIAL_VISIBLE); const hiddenSessionCount = Math.max(0, sessions.length - INITIAL_VISIBLE); const handleCreateGoal = async () => { const title = newGoalTitle.trim(); if (!title) return; setCreatingGoal(true); try { await rpc.projectGoals.create({ sectionId, title, groupId, }); setNewGoalTitle(""); await reloadGoals(); } finally { setCreatingGoal(false); } }; return (

Project

{sectionName}

Goals for this project, separate from chat messages. Conversations are below.

Goals

Goal
{loadingGoals ? (

Loading goals…

) : goals.length === 0 ? (

No goals in this project yet.

) : (
{goals.map((goal) => { const tone = goalStatusTone(goal.status); const label = goalStatusLabel(goal.status); const linkedGroupId = goal.groupId ?? groupId; return (
Goal

{goal.title}

{goal.description ? (

{goal.description}

) : null}
{formatRelativeTime(goal.updatedAt)}
{/voice\s*first/i.test(goal.title) ? ( ) : null}
); })}
)}
setNewGoalTitle(event.target.value)} placeholder={t`New goal…`} className="min-w-0 flex-1 rounded-xl border border-border bg-background px-3 py-2 text-ui-body-sm text-foreground" onKeyDown={(event) => { if (event.key === "Enter") void handleCreateGoal(); }} />

Conversations

{groupName}
{loadingSessions ? (

Loading conversations…

) : sessions.length === 0 ? (

No conversations in this project chat yet.

) : (
{visibleSessions.map((session) => { const tone = sessionStatusTone(session.status); const label = sessionStatusLabel(session.status); const participants = session.botNames.length > 0 ? session.botNames.join(", ") : t`Team`; return ( ); })} {!sessionsExpanded && hiddenSessionCount > 0 ? ( ) : null}
)}
); }