From 35e01cb1596240c2e9a0562efcbbcd4c509ae19e Mon Sep 17 00:00:00 2001 From: Jaydeep Rusia Date: Fri, 29 May 2026 20:05:09 +0530 Subject: [PATCH] Refine check-in flow and history --- backend/server.py | 226 +++++++++++- frontend/src/lib/api.js | 13 + .../src/pages/IdeaDetail.checkin.test.jsx | 115 ++++++ frontend/src/pages/IdeaDetail.jsx | 338 ++++++++++++++---- 4 files changed, 611 insertions(+), 81 deletions(-) create mode 100644 frontend/src/pages/IdeaDetail.checkin.test.jsx diff --git a/backend/server.py b/backend/server.py index b13a52b..ba64aa2 100644 --- a/backend/server.py +++ b/backend/server.py @@ -28,7 +28,7 @@ import secrets import httpx from pydantic import BaseModel, Field -from typing import List, Optional, Literal +from typing import Any, List, Optional, Literal from datetime import datetime, timezone, timedelta from llm_provider import run_llm, run_llm_cheap @@ -148,7 +148,7 @@ async def _check_diagnosis_gate(user_id: str, idea_id: str): {"idea_id": idea_id, "user_id": user_id}, {"_id": 0}, sort=[("created_at", -1)] ) if latest_diag: - actions = latest_diag.get("this_week_actions", []) + actions: List[dict[str, Any]] = list(latest_diag.get("this_week_actions") or []) if not any(a.get("done") for a in actions): raise HTTPException( status_code=400, @@ -367,6 +367,13 @@ class CheckinCreate(BaseModel): blockers: str = "" +class CheckinUpdate(BaseModel): + actions_completed: List[str] = [] + what_changed: str = "" + new_learnings: str = "" + blockers: str = "" + + class Action(BaseModel): action_id: str title: str @@ -404,10 +411,10 @@ class Checkin(BaseModel): checkin_id: str idea_id: str user_id: str - actions_completed: List[str] what_changed: str new_learnings: str blockers: str + diagnosis_id: Optional[str] = None delta_summary: str = "" # AI-generated "what changed since last week" created_at: datetime @@ -484,6 +491,26 @@ def _serialize_timeline_doc(doc: Optional[dict]) -> Optional[dict]: ) +def _serialize_checkin(doc: dict) -> dict: + v = doc.get("created_at") + if isinstance(v, str): + try: + doc["created_at"] = datetime.fromisoformat(v) + except Exception: + pass + return _add_ist_fields(doc, ["created_at"]) + + +def _build_action_snapshot( + diag: Optional[dict[str, Any]], completed_action_ids: List[str] +) -> List[dict[str, Any]]: + actions: List[dict[str, Any]] = [] + completed = set(completed_action_ids or []) + for action in (diag or {}).get("this_week_actions", []): + actions.append({**action, "done": action.get("action_id") in completed}) + return actions + + @api.post("/ideas") async def create_idea( payload: IdeaCreate, @@ -710,7 +737,11 @@ async def _parse_or_repair_json(raw: str, session_id: str) -> dict: def _build_idea_brief( - user: User, idea: dict, prior_diag: Optional[dict], last_checkin: Optional[dict] + user: User, + idea: dict, + prior_diag: Optional[dict], + last_checkin: Optional[dict], + last_checkin_diag: Optional[dict], ) -> str: lines = [ f"FOUNDER: {user.name}", @@ -733,10 +764,18 @@ def _build_idea_brief( f"- Previous key insight: {prior_diag.get('key_insight')}", ] if last_checkin: + completed_actions = [] + pending_actions = [] + for action in (last_checkin_diag or {}).get("this_week_actions") or []: + if action.get("done"): + completed_actions.append(action.get("title")) + else: + pending_actions.append(action.get("title")) lines += [ "", "LAST CHECK-IN:", - f"- Actions completed: {last_checkin.get('actions_completed')}", + f"- Actions completed: {completed_actions or '(none)'}", + f"- Actions not completed: {pending_actions or '(none)'}", f"- What changed: {last_checkin.get('what_changed')}", f"- New learnings: {last_checkin.get('new_learnings')}", f"- Blockers: {last_checkin.get('blockers')}", @@ -815,7 +854,17 @@ async def _run_diagnosis_job(job_id: str, idea_id: str, user_id: str): last_checkin = await db.checkins.find_one( {"idea_id": idea_id}, {"_id": 0}, sort=[("created_at", -1)] ) - brief = _build_idea_brief(user, idea, prior, last_checkin) + last_checkin_diag = None + if last_checkin and last_checkin.get("diagnosis_id"): + last_checkin_diag = await db.diagnoses.find_one( + { + "idea_id": idea_id, + "user_id": user_id, + "diagnosis_id": last_checkin["diagnosis_id"], + }, + {"_id": 0}, + ) + brief = _build_idea_brief(user, idea, prior, last_checkin, last_checkin_diag) parsed = await asyncio.wait_for( _run_diagnosis_llm(brief, f"diag_{idea_id}_{uuid.uuid4().hex[:8]}"), @@ -910,7 +959,7 @@ async def toggle_action( ) if not diag: raise HTTPException(404, "No diagnosis") - actions = diag.get("this_week_actions", []) + actions: List[dict[str, Any]] = list(diag.get("this_week_actions") or []) for a in actions: if a.get("action_id") == action_id: a["done"] = not a.get("done", False) @@ -957,16 +1006,27 @@ async def create_checkin( streak = 1 checkin_id = f"cki_{uuid.uuid4().hex[:12]}" + # find the most recent diagnosis that does NOT yet have a linked checkin + latest_diag = None + async for d in db.diagnoses.find({"idea_id": idea_id}).sort("created_at", -1): + # check if any checkin already links to this diagnosis + exists = await db.checkins.find_one( + {"diagnosis_id": d.get("diagnosis_id")}, {"_id": 1} + ) + if not exists: + latest_diag = d + break doc = { "checkin_id": checkin_id, "idea_id": idea_id, "user_id": user.user_id, - "actions_completed": payload.actions_completed, "what_changed": payload.what_changed, "new_learnings": payload.new_learnings, "blockers": payload.blockers, "delta_summary": "", "created_at": now.isoformat(), + # keep the check-in record self-contained in the checkins collection + "diagnosis_id": latest_diag.get("diagnosis_id") if latest_diag else None, } await db.checkins.insert_one({**doc}) await db.ideas.update_one( @@ -979,21 +1039,153 @@ async def create_checkin( } }, ) - # Also mark completed actions as done in latest diagnosis + # Also mark completed actions as done in latest diagnosis (if present) + if latest_diag: + actions = _build_action_snapshot(latest_diag, payload.actions_completed) + await db.diagnoses.update_one( + {"diagnosis_id": latest_diag["diagnosis_id"]}, + {"$set": {"this_week_actions": actions}}, + ) + return _serialize_timeline_doc({**doc, "streak": streak}) + + +@api.get("/ideas/{idea_id}/checkins") +async def list_checkins( + idea_id: str, + request: Request, + limit: int = 20, + before: Optional[str] = None, + session_token_cookie: Optional[str] = Cookie(None, alias="session_token"), + authorization: Optional[str] = Header(None), +): + user = await get_current_user(request, session_token_cookie, authorization) + idea = await db.ideas.find_one( + {"idea_id": idea_id, "user_id": user.user_id, "status": {"$ne": "archived"}}, + {"_id": 0, "idea_id": 1}, + ) + if not idea: + raise HTTPException(404, "Idea not found") + + limit = max(1, min(limit, 100)) + query: dict[str, Any] = {"idea_id": idea_id, "user_id": user.user_id} + if before: + query["created_at"] = {"$lt": before} + + docs = ( + await db.checkins.find(query, {"_id": 0}) + .sort("created_at", -1) + .to_list(limit + 1) + ) + has_more = len(docs) > limit + items = docs[:limit] + next_before = items[-1].get("created_at") if has_more and items else None + + enriched_items = [] + for item in items: + enriched = dict(item) + if not enriched.get("diagnosis_actions") and item.get("diagnosis_id"): + diag = await db.diagnoses.find_one( + {"idea_id": idea_id, "user_id": user.user_id, "diagnosis_id": item["diagnosis_id"]}, + {"_id": 0, "this_week_actions": 1}, + ) + enriched["diagnosis_actions"] = diag.get("this_week_actions", []) if diag else [] + else: + enriched["diagnosis_actions"] = enriched.get("diagnosis_actions", []) + enriched_items.append(enriched) + + return { + "items": [_serialize_checkin(i) for i in enriched_items], + "has_more": has_more, + "next_before": next_before, + } + + +@api.patch("/ideas/{idea_id}/checkins/{checkin_id}") +async def update_checkin( + idea_id: str, + checkin_id: str, + payload: CheckinUpdate, + request: Request, + session_token_cookie: Optional[str] = Cookie(None, alias="session_token"), + authorization: Optional[str] = Header(None), +): + user = await get_current_user(request, session_token_cookie, authorization) + idea = await db.ideas.find_one( + {"idea_id": idea_id, "user_id": user.user_id, "status": {"$ne": "archived"}}, + {"_id": 0, "idea_id": 1}, + ) + if not idea: + raise HTTPException(404, "Idea not found") + + latest = await db.checkins.find_one( + {"idea_id": idea_id, "user_id": user.user_id}, + {"_id": 0, "checkin_id": 1}, + sort=[("created_at", -1)], + ) + if not latest: + raise HTTPException(404, "Check-in not found") + if latest.get("checkin_id") != checkin_id: + raise HTTPException( + status_code=400, + detail="Only the latest check-in can be edited.", + ) + + existing = await db.checkins.find_one( + {"idea_id": idea_id, "user_id": user.user_id, "checkin_id": checkin_id}, + {"_id": 0}, + ) + if not existing: + raise HTTPException(404, "Check-in not found") + latest_diag = await db.diagnoses.find_one( - {"idea_id": idea_id}, {"_id": 0}, sort=[("created_at", -1)] + {"idea_id": idea_id, "user_id": user.user_id}, + {"_id": 0, "diagnosis_id": 1, "this_week_actions": 1}, + sort=[("created_at", -1)], + ) + if existing.get("diagnosis_id") != (latest_diag or {}).get("diagnosis_id"): + raise HTTPException( + status_code=400, + detail="Only the check-in for the latest diagnosis can be edited.", + ) + + updated = { + "what_changed": payload.what_changed, + "new_learnings": payload.new_learnings, + "blockers": payload.blockers, + } + linked_diag = None + if existing.get("diagnosis_id"): + linked_diag = await db.diagnoses.find_one( + { + "idea_id": idea_id, + "user_id": user.user_id, + "diagnosis_id": existing["diagnosis_id"], + }, + {"_id": 0, "this_week_actions": 1}, + ) + if not linked_diag: + linked_diag = await db.diagnoses.find_one( + {"idea_id": idea_id, "user_id": user.user_id}, + {"_id": 0, "this_week_actions": 1}, + sort=[("created_at", -1)], + ) + + await db.checkins.update_one( + {"checkin_id": checkin_id, "idea_id": idea_id, "user_id": user.user_id}, + { + "$set": updated + }, ) + if latest_diag: - actions = latest_diag.get("this_week_actions", []) - completed = set(payload.actions_completed or []) - for a in actions: - if a.get("action_id") in completed: - a["done"] = True + actions = _build_action_snapshot(latest_diag, payload.actions_completed) await db.diagnoses.update_one( {"diagnosis_id": latest_diag["diagnosis_id"]}, {"$set": {"this_week_actions": actions}}, ) - return _serialize_timeline_doc({**doc, "streak": streak}) + + out = {**existing, **updated} + return _serialize_checkin(out) @api.get("/dashboard/summary") @@ -1168,7 +1360,7 @@ async def build_shared_diagnosis_content(slug: str) -> dict: "why": a.get("why"), "effort": a.get("effort"), } - for a in diag.get("this_week_actions", []) + for a in list(diag.get("this_week_actions") or []) ], "created_at": diag["created_at"], "created_at_ist": _to_ist_iso(diag["created_at"]), diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 7d1edb4..a8c360c 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -101,6 +101,19 @@ export async function createCheckin(idea_id, payload) { return data; } +export async function listCheckins(idea_id, params = {}) { + const { data } = await api.get(`/ideas/${idea_id}/checkins`, { params }); + return data; +} + +export async function updateCheckin(idea_id, checkin_id, payload) { + const { data } = await api.patch( + `/ideas/${idea_id}/checkins/${checkin_id}`, + payload, + ); + return data; +} + export async function deleteIdea(idea_id) { const { data } = await api.delete(`/ideas/${idea_id}`); return data; diff --git a/frontend/src/pages/IdeaDetail.checkin.test.jsx b/frontend/src/pages/IdeaDetail.checkin.test.jsx new file mode 100644 index 0000000..10308a4 --- /dev/null +++ b/frontend/src/pages/IdeaDetail.checkin.test.jsx @@ -0,0 +1,115 @@ +// Ensure React's act environment is enabled for concurrent rendering in tests +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; + +jest.mock( + "react-router-dom", + () => ({ + useNavigate: () => jest.fn(), + useParams: () => ({}), + useSearchParams: () => [new URLSearchParams(), jest.fn()], + }), + { virtual: true }, +); + +jest.mock( + "@/lib/track", + () => ({ + track: jest.fn(), + }), + { virtual: true }, +); + +const { CheckinDialog } = require("./IdeaDetail"); + +function renderDialog(props) { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render(); + }); + + return { + container, + unmount: () => { + act(() => { + root.unmount(); + }); + document.body.removeChild(container); + }, + }; +} + +describe("CheckinDialog", () => { + const baseProps = { + actions: [ + { action_id: "a1", title: "Call users", done: false }, + { action_id: "a2", title: "Ship page", done: true }, + ], + diag: { + this_week_actions: [ + { action_id: "a1", done: false }, + { action_id: "a2", done: true }, + ], + }, + diagnosisLimits: { unlimited: false, weekly_remaining: 1 }, + cooldownLeft: 0, + latestCheckin: { + checkin_id: "cki_latest", + actions_completed: ["a1"], + what_changed: "Latest change", + new_learnings: "Latest learning", + blockers: "Latest blocker", + }, + initialCheckin: null, + isEditing: false, + onClose: () => {}, + onSubmit: async () => {}, + }; + + test("new check-in starts blank", () => { + const view = renderDialog(baseProps); + + const whatChanged = view.container.querySelector( + '[data-testid="checkin-changed"]', + ); + // New check-ins should start blank regardless of latestCheckin + expect(whatChanged.value).toBe(""); + view.unmount(); + }); + + test("edit mode hides start mode and uses save label", () => { + const view = renderDialog({ + ...baseProps, + isEditing: true, + initialCheckin: { + checkin_id: "cki_latest", + actions_completed: ["a2"], + what_changed: "Edited value", + new_learnings: "Edited learnings", + blockers: "Edited blockers", + }, + }); + + // Start mode UI removed; ensure no start-mode button exists + expect( + view.container.querySelector('[data-testid="checkin-mode-blank"]'), + ).toBeNull(); + + const submitBtn = view.container.querySelector( + '[data-testid="checkin-submit"]', + ); + expect(submitBtn.textContent).toContain("Save changes"); + + const whatChanged = view.container.querySelector( + '[data-testid="checkin-changed"]', + ); + expect(whatChanged.value).toBe("Edited value"); + view.unmount(); + }); +}); diff --git a/frontend/src/pages/IdeaDetail.jsx b/frontend/src/pages/IdeaDetail.jsx index bb8bfcd..ad08c0a 100644 --- a/frontend/src/pages/IdeaDetail.jsx +++ b/frontend/src/pages/IdeaDetail.jsx @@ -2,9 +2,11 @@ import React, { useEffect, useRef, useState } from "react"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { getIdea, + listCheckins, diagnose, toggleAction, createCheckin, + updateCheckin, deleteIdea, waitForDiagnosis, createShareLink, @@ -42,7 +44,7 @@ function formatDuration(seconds = 0) { .join(":"); } -function getRecheckState(diag, diagnosisLimits, cooldownLeft) { +export function getRecheckState(diag, diagnosisLimits, cooldownLeft) { if (diagnosisLimits?.unlimited) { return { canRecheck: true, @@ -195,25 +197,21 @@ function ActionCard({ a, idx, onToggle }) { ); } -function CheckinDialog({ +export function CheckinDialog({ actions, diag, diagnosisLimits, cooldownLeft, latestCheckin, + initialCheckin, + isEditing, onClose, onSubmit, }) { - const [completed, setCompleted] = useState( - actions.filter((a) => a.done).map((a) => a.action_id), - ); - const [whatChanged, setWhatChanged] = useState( - latestCheckin?.what_changed || "", - ); - const [learnings, setLearnings] = useState( - latestCheckin?.new_learnings || "", - ); - const [blockers, setBlockers] = useState(latestCheckin?.blockers || ""); + const [completed, setCompleted] = useState([]); + const [whatChanged, setWhatChanged] = useState(""); + const [learnings, setLearnings] = useState(""); + const [blockers, setBlockers] = useState(""); const [busy, setBusy] = useState(false); const previewDiag = diag ? { @@ -231,11 +229,21 @@ function CheckinDialog({ ); useEffect(() => { - setCompleted(actions.filter((a) => a.done).map((a) => a.action_id)); - setWhatChanged(latestCheckin?.what_changed || ""); - setLearnings(latestCheckin?.new_learnings || ""); - setBlockers(latestCheckin?.blockers || ""); - }, [actions, latestCheckin]); + // For new check-ins always start blank. When editing, load the initial checkin. + const source = isEditing ? initialCheckin : null; + const defaultCompleted = actions + .filter((a) => a.done) + .map((a) => a.action_id); + const sourceCompleted = Array.isArray(source?.diagnosis_actions) + ? source.diagnosis_actions + .filter((action) => action.done) + .map((action) => action.action_id) + : []; + setCompleted(sourceCompleted.length > 0 ? sourceCompleted : defaultCompleted); + setWhatChanged(source?.what_changed || ""); + setLearnings(source?.new_learnings || ""); + setBlockers(source?.blockers || ""); + }, [actions, initialCheckin, isEditing]); const toggle = (id) => { setCompleted((c) => @@ -275,7 +283,7 @@ function CheckinDialog({ )}

- What actually happened? + {isEditing ? "Edit latest check-in" : "What actually happened?"}

{previewRecheck.canRecheck @@ -283,6 +291,8 @@ function CheckinDialog({ : "Your check-in will still be saved. Re-diagnosis unlocks when the re-diagnosis timer does."}

+ {/* Removed prefill/start-mode: new check-ins always start blank */} +
actions completed
@@ -364,6 +374,8 @@ function CheckinDialog({ {" "} Kicking off… + ) : isEditing ? ( + "Save changes" ) : previewRecheck.canRecheck ? ( "Submit & re-diagnose ->" ) : ( @@ -568,8 +580,14 @@ export default function IdeaDetail() { const [search, setSearch] = useSearchParams(); const navigate = useNavigate(); const [data, setData] = useState(null); + const [checkins, setCheckins] = useState([]); + const [checkinsHasMore, setCheckinsHasMore] = useState(false); + const [checkinsCursor, setCheckinsCursor] = useState(null); + const [checkinsLoadingMore, setCheckinsLoadingMore] = useState(false); + const [expandedCheckinId, setExpandedCheckinId] = useState(null); const [loading, setLoading] = useState(true); const [checkinOpen, setCheckinOpen] = useState(false); + const [editingCheckin, setEditingCheckin] = useState(null); const [shareOpen, setShareOpen] = useState(false); const [diagnosing, setDiagnosing] = useState(false); const [error, setError] = useState(""); @@ -580,14 +598,37 @@ export default function IdeaDetail() { const load = async () => { try { - const d = await getIdea(idea_id); + const [d, checkinPage] = await Promise.all([ + getIdea(idea_id), + listCheckins(idea_id, { limit: 20 }), + ]); setData(d); + setCheckins(checkinPage?.items || []); + setExpandedCheckinId(checkinPage?.items?.[0]?.checkin_id || null); + setCheckinsHasMore(Boolean(checkinPage?.has_more)); + setCheckinsCursor(checkinPage?.next_before || null); return d; } finally { setLoading(false); } }; + const loadMoreCheckins = async () => { + if (!checkinsHasMore || checkinsLoadingMore || !checkinsCursor) return; + setCheckinsLoadingMore(true); + try { + const page = await listCheckins(idea_id, { + limit: 20, + before: checkinsCursor, + }); + setCheckins((prev) => [...prev, ...(page?.items || [])]); + setCheckinsHasMore(Boolean(page?.has_more)); + setCheckinsCursor(page?.next_before || null); + } finally { + setCheckinsLoadingMore(false); + } + }; + useEffect(() => { load().then((d) => { if (d?.latest_diagnosis) { @@ -660,7 +701,11 @@ export default function IdeaDetail() { const idea = data.idea; const diag = data.latest_diagnosis; - const latestCheckin = data.checkins?.[0] || null; + const latestCheckin = checkins[0] || null; + const diagCheckins = checkins.filter((c) => !!c.diagnosis_id); + const activeCheckin = diag + ? checkins.find((c) => c.diagnosis_id === diag.diagnosis_id) || null + : null; const style = verdictStyle(diag?.verdict); const { canRecheck, label: recheckLabel } = getRecheckState( diag, @@ -687,14 +732,23 @@ export default function IdeaDetail() { setError(""); setNotice(""); try { - await createCheckin(idea_id, payload); + if (editingCheckin?.checkin_id) { + await updateCheckin(idea_id, editingCheckin.checkin_id, payload); + } else { + await createCheckin(idea_id, payload); + } } catch (e) { setError(errorMessage(e, "Check-in failed")); return; } + setEditingCheckin(null); setCheckinOpen(false); - setNotice("Weekly check-in saved."); + setNotice( + editingCheckin + ? "Latest check-in updated." + : "Weekly check-in saved.", + ); try { const refreshed = await load(); @@ -705,15 +759,19 @@ export default function IdeaDetail() { refreshed?.diagnosis_limits, nextCooldownLeft, ); - if (nextState.canRecheck) { + if (!editingCheckin && nextState.canRecheck) { await runDiagnose(); return; } if (nextState.label) { - setNotice(`Weekly check-in saved. ${nextState.label}`); + setNotice( + editingCheckin + ? "Latest check-in updated." + : `Weekly check-in saved. ${nextState.label}`, + ); } } catch (e) { - setNotice("Weekly check-in saved."); + setNotice(editingCheckin ? "Latest check-in updated." : "Weekly check-in saved."); setError( errorMessage(e, "Saved your check-in, but couldn't refresh the page"), ); @@ -785,15 +843,24 @@ export default function IdeaDetail() {

{idea.one_liner}

- {diag && !hasPending && ( + {!hasPending && ( <> + + )} + {diag && !hasPending && ( + <>
)} - {!hasPending && tab === "history" && ( -
- {data.diagnoses.map((d, i) => { - const s = verdictStyle(d.verdict); - const date = new Date(d.created_at_ist || d.created_at); - return ( -
-
- - #{data.diagnoses.length - i} ·{" "} - {date.toLocaleDateString("en-IN", { - timeZone: "Asia/Kolkata", - })} - - {s.label} - - FMF {d.fmf_score} · WTP {d.wtp_score} · MOM{" "} - {d.momentum_score} - + {!hasPending && tab === "checkins" && ( +
+
+
weekly check-ins
+
+ {diagCheckins.length === 0 && ( +
+ No check-ins yet.
-
{ + const date = new Date(c.created_at_ist || c.created_at); + const isOpen = expandedCheckinId === c.checkin_id; + return ( +
+ + {isOpen && ( +
+
+
+
what changed
+

+ {c.what_changed || "-"} +

+
+
+
learnings
+

+ {c.new_learnings || "-"} +

+
+
+
blockers
+

+ {c.blockers || "-"} +

+
+
+ {Array.isArray(c.diagnosis_actions) && c.diagnosis_actions.length > 0 && ( +
+
actions
+
+ {c.diagnosis_actions.map((action) => ( +
+
+
+
+ {action.title} +
+ {action.why && ( +
+ {action.why} +
+ )} +
+ + {action.done ? "complete" : "incomplete"} + +
+
+ ))} +
+
+ )} +
+ )} +
+ ); + })} +
+ {checkinsHasMore && ( +
+
-

- {d.key_insight} -

+ {checkinsLoadingMore ? "Loading..." : "Load older check-ins"} +
- ); - })} + )} +
+
+ )} + + {!hasPending && tab === "evolution" && ( +
+
+
diagnosis evolution
+
+ {data.diagnoses.map((d, i) => { + const s = verdictStyle(d.verdict); + const date = new Date(d.created_at_ist || d.created_at); + return ( +
+
+ + #{data.diagnoses.length - i} ·{" "} + {date.toLocaleDateString("en-IN", { + timeZone: "Asia/Kolkata", + })} + + {s.label} + + FMF {d.fmf_score} · WTP {d.wtp_score} · MOM{" "} + {d.momentum_score} + +
+
+ {d.verdict_headline} +
+

+ {d.key_insight} +

+
+ ); + })} + {data.diagnoses.length === 0 && ( +
+ No diagnoses yet. +
+ )} +
+
)}
- {checkinOpen && diag && ( + {checkinOpen && ( setCheckinOpen(false)} + initialCheckin={editingCheckin} + isEditing={Boolean(editingCheckin)} + onClose={() => { + setCheckinOpen(false); + setEditingCheckin(null); + }} onSubmit={onCheckinSubmit} /> )}