From 8e58b123bf0b86cc0f264397f86682defe2189a3 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 11 Feb 2026 20:42:16 +0000 Subject: [PATCH] docs(frontend): add JSDoc for complex UI utilities --- .../src/components/BoardApprovalsPanel.tsx | 34 +++++++++++++++++++ .../src/components/BoardOnboardingChat.tsx | 22 ++++++++++++ frontend/src/components/charts/chart.tsx | 10 ++++++ .../src/components/organisms/TaskBoard.tsx | 26 ++++++++++++++ .../src/components/ui/dropdown-select.tsx | 12 +++++-- 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/BoardApprovalsPanel.tsx b/frontend/src/components/BoardApprovalsPanel.tsx index eaaeeab6..16c46872 100644 --- a/frontend/src/components/BoardApprovalsPanel.tsx +++ b/frontend/src/components/BoardApprovalsPanel.tsx @@ -148,9 +148,15 @@ const formatRubricTooltipValue = ( ); }; +/** + * Narrow unknown values to a plain record. + * + * Used for defensive parsing of `approval.payload` (schema can evolve). + */ const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); +/** Safely read any value at a nested path inside an approval payload. */ const payloadAtPath = (payload: Approval["payload"], path: string[]) => { let current: unknown = payload; for (const key of path) { @@ -160,6 +166,12 @@ const payloadAtPath = (payload: Approval["payload"], path: string[]) => { return current ?? null; }; +/** + * Safely read a simple scalar value from an approval payload. + * + * The backend payload shape can evolve (camelCase vs snake_case). Keeping these + * helpers centralized makes it easier to support older approvals. + */ const payloadValue = (payload: Approval["payload"], key: string) => { const value = payloadAtPath(payload, [key]); if (typeof value === "string" || typeof value === "number") { @@ -168,12 +180,18 @@ const payloadValue = (payload: Approval["payload"], key: string) => { return null; }; +/** + * Safely read a string[] value from an approval payload. + * + * Filters non-string entries to keep UI rendering predictable. + */ const payloadValues = (payload: Approval["payload"], key: string) => { const value = payloadAtPath(payload, [key]); if (!Array.isArray(value)) return []; return value.filter((item): item is string => typeof item === "string"); }; +/** Safely read a scalar value from an approval payload at a nested path. */ const payloadNestedValue = (payload: Approval["payload"], path: string[]) => { const value = payloadAtPath(payload, path); if (typeof value === "string" || typeof value === "number") { @@ -182,6 +200,7 @@ const payloadNestedValue = (payload: Approval["payload"], path: string[]) => { return null; }; +/** Safely read a string[] value from an approval payload at a nested path. */ const payloadNestedValues = (payload: Approval["payload"], path: string[]) => { const value = payloadAtPath(payload, path); if (!Array.isArray(value)) return []; @@ -222,6 +241,15 @@ const normalizeRubricScores = (raw: unknown): Record => { const payloadRubricScores = (payload: Approval["payload"]) => normalizeRubricScores(payloadAtPath(payload, ["analytics", "rubric_scores"])); +/** + * Extract task ids referenced by an approval. + * + * Approvals can reference tasks in multiple places depending on the producer: + * - top-level `task_id` / `task_ids` fields + * - nested payload keys (task_id/taskId/taskIDs, etc.) + * + * We merge/dedupe to get a best-effort list for UI deep links. + */ const approvalTaskIds = (approval: Approval) => { const payload = approval.payload ?? {}; const linkedTaskIds = (approval as Approval & { task_ids?: string[] | null }) @@ -318,6 +346,12 @@ const approvalRelatedTasks = (approval: Approval): RelatedTaskSummary[] => { const taskHref = (boardId: string, taskId: string) => `/boards/${encodeURIComponent(boardId)}?taskId=${encodeURIComponent(taskId)}`; +/** + * Create a small, human-readable summary of an approval request. + * + * Used by the approvals panel modal: it prefers explicit fields but falls back + * to payload-derived values so older approvals still render well. + */ const approvalSummary = (approval: Approval, boardLabel?: string | null) => { const payload = approval.payload ?? {}; const taskIds = approvalTaskIds(approval); diff --git a/frontend/src/components/BoardOnboardingChat.tsx b/frontend/src/components/BoardOnboardingChat.tsx index 7b2db557..ac5c95a9 100644 --- a/frontend/src/components/BoardOnboardingChat.tsx +++ b/frontend/src/components/BoardOnboardingChat.tsx @@ -30,6 +30,12 @@ type NormalizedMessage = { content: string; }; +/** + * Normalize backend onboarding messages into a strict `{role, content}` list. + * + * The server stores messages as untyped JSON; this protects the UI from partial + * or malformed entries. + */ const normalizeMessages = ( value?: BoardOnboardingReadMessages, ): NormalizedMessage[] | null => { @@ -59,6 +65,16 @@ const FREE_TEXT_OPTION_RE = const isFreeTextOption = (label: string) => FREE_TEXT_OPTION_RE.test(label); +/** + * Best-effort parser for assistant-produced question payloads. + * + * During onboarding, the assistant can respond with either: + * - raw JSON (ideal) + * - a fenced ```json block + * - slightly-structured objects + * + * This function validates shape and normalizes option ids/labels. + */ const normalizeQuestion = (value: unknown): Question | null => { if (!value || typeof value !== "object") return null; const data = value as { question?: unknown; options?: unknown }; @@ -90,6 +106,12 @@ const normalizeQuestion = (value: unknown): Question | null => { return { question: data.question, options }; }; +/** + * Extract the most recent assistant question from the transcript. + * + * We intentionally only inspect the last assistant message: the user may have + * typed arbitrary text between questions. + */ const parseQuestion = (messages?: NormalizedMessage[] | null) => { if (!messages?.length) return null; const lastAssistant = [...messages] diff --git a/frontend/src/components/charts/chart.tsx b/frontend/src/components/charts/chart.tsx index e0056c46..46ebfc11 100644 --- a/frontend/src/components/charts/chart.tsx +++ b/frontend/src/components/charts/chart.tsx @@ -40,6 +40,12 @@ function useChart() { return context; } +/** + * Recharts wrapper that: + * - Provides a shared `ChartConfig` via context (labels/icons/colors) + * - Exposes a small legend state (hide/toggle series) + * - Injects CSS variables (`--color-*`) scoped to this chart instance + */ function ChartContainer({ id, className, @@ -108,6 +114,10 @@ function ChartContainer({ ); } +/** + * Emits scoped theme-aware CSS variables so Recharts series can use + * `var(--color-)` without hardcoding colors in every chart. + */ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( ([, config]) => config.theme || config.color, diff --git a/frontend/src/components/organisms/TaskBoard.tsx b/frontend/src/components/organisms/TaskBoard.tsx index 00c342b6..6eb6ebee 100644 --- a/frontend/src/components/organisms/TaskBoard.tsx +++ b/frontend/src/components/organisms/TaskBoard.tsx @@ -82,15 +82,25 @@ const columns: Array<{ }, ]; +/** + * Build compact due-date UI state for a task card. + * + * - Returns `due: undefined` when the task has no due date (or it's invalid), so + * callers can omit the due-date UI entirely. + * - Treats a task as overdue only if it is not `done` (so "Done" tasks don't + * keep showing as overdue forever). + */ const resolveDueState = ( task: Task, ): { due: string | undefined; isOverdue: boolean } => { const date = parseApiDatetime(task.due_at); if (!date) return { due: undefined, isOverdue: false }; + const dueLabel = date.toLocaleDateString(undefined, { month: "short", day: "numeric", }); + const isOverdue = task.status !== "done" && date.getTime() < Date.now(); return { due: isOverdue ? `Overdue ยท ${dueLabel}` : dueLabel, @@ -103,6 +113,16 @@ type CardPosition = { left: number; top: number }; const KANBAN_MOVE_ANIMATION_MS = 240; const KANBAN_MOVE_EASING = "cubic-bezier(0.2, 0.8, 0.2, 1)"; +/** + * Kanban-style task board with 4 columns. + * + * Notes: + * - Uses a lightweight FLIP animation (via `useLayoutEffect`) to animate cards + * to their new positions when tasks move between columns. + * - Drag interactions can temporarily fight browser-managed drag images; the + * animation is disabled while a card is being dragged. + * - Respects `prefers-reduced-motion`. + */ export const TaskBoard = memo(function TaskBoard({ tasks, onTaskSelect, @@ -131,6 +151,12 @@ export const TaskBoard = memo(function TaskBoard({ [], ); + /** + * Snapshot each card's position relative to the scroll container. + * + * We store these measurements so we can compute deltas (prev - next) and + * apply the FLIP technique on the next render. + */ const measurePositions = useCallback((): Map => { const positions = new Map(); const container = boardRef.current; diff --git a/frontend/src/components/ui/dropdown-select.tsx b/frontend/src/components/ui/dropdown-select.tsx index 178fba62..51b81570 100644 --- a/frontend/src/components/ui/dropdown-select.tsx +++ b/frontend/src/components/ui/dropdown-select.tsx @@ -40,7 +40,11 @@ type DropdownSelectProps = { emptyMessage?: string; }; -// Resolve trigger placeholder text with explicit prop override first, then accessible fallback. +/** + * Derive a human-friendly trigger placeholder from an accessible `ariaLabel`. + * + * Keeps placeholder strings consistent even when callers only provide aria text. + */ const resolvePlaceholder = (ariaLabel: string, placeholder?: string) => { if (placeholder) { return placeholder; @@ -52,7 +56,11 @@ const resolvePlaceholder = (ariaLabel: string, placeholder?: string) => { return trimmed.endsWith("...") ? trimmed : `${trimmed}...`; }; -// Resolve search input placeholder from explicit override or a normalized aria label. +/** + * Search input placeholder derived from `ariaLabel`. + * + * Example: ariaLabel="Select agent" -> "Search agent...". + */ const resolveSearchPlaceholder = ( ariaLabel: string, searchPlaceholder?: string,