diff --git a/web/app/components/base/markdown-blocks/code-block.tsx b/web/app/components/base/markdown-blocks/code-block.tsx index 79a374e291..28d460a1c6 100644 --- a/web/app/components/base/markdown-blocks/code-block.tsx +++ b/web/app/components/base/markdown-blocks/code-block.tsx @@ -16,6 +16,7 @@ import { Theme } from '@/types/app' import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false }) +const QuadrantMatrix = dynamic(() => import('@/app/components/base/quadrant-matrix'), { ssr: false }) // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD const capitalizationLanguageNameMap: Record = { @@ -40,6 +41,7 @@ const capitalizationLanguageNameMap: Record = { latex: 'Latex', svg: 'SVG', abc: 'ABC', + quadrant: 'Quadrant', } const getCorrectCapitalizationLanguageName = (language: string) => { if (!language) @@ -409,6 +411,12 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any ) + case 'quadrant': + return ( + + + + ) default: return ( = ({ content }) => { + const parsedData = useMemo(() => { + try { + const trimmed = content.trim() + const data = JSON.parse(trimmed) + + if (!isValidQuadrantData(data)) + return null + + return data + } + catch { + return null + } + }, [content]) + + if (!parsedData) { + return ( +
+
+
Invalid Quadrant Data
+
+ Expected JSON format with q1, q2, q3, q4 arrays +
+
+
+ ) + } + + const totalTasks + = parsedData.q1.length + + parsedData.q2.length + + parsedData.q3.length + + parsedData.q4.length + + return ( +
+ {/* Header */} +
+
+
+ Eisenhower Matrix +
+
+ {totalTasks} + {' '} + task + {totalTasks !== 1 ? 's' : ''} + {' '} + across 4 quadrants +
+
+
+ I + =Importance + U + =Urgency +
+
+ + {/* Axis Labels */} +
+ {/* Importance Label (Top Center) */} +
+ + Important + +
+ + {/* Main Grid with Urgency Labels */} +
+ {/* Left: Not Urgent Label */} +
+ + Not Urgent + +
+ + {/* Center: 2x2 Grid */} +
+
+ {/* Row 1: Important */} + + + + {/* Row 2: Not Important */} + + +
+
+ + {/* Right: Urgent Label */} +
+ + Urgent + +
+
+ + {/* Not Important Label (Bottom Center) */} +
+ + Not Important + +
+
+
+ ) +} + +export default QuadrantMatrix diff --git a/web/app/components/base/quadrant-matrix/quadrant-card.tsx b/web/app/components/base/quadrant-matrix/quadrant-card.tsx new file mode 100644 index 0000000000..52918289ac --- /dev/null +++ b/web/app/components/base/quadrant-matrix/quadrant-card.tsx @@ -0,0 +1,64 @@ +'use client' +import type { FC } from 'react' +import type { QuadrantConfig, Task } from './types' +import { cn } from '@/utils/classnames' +import TaskItem from './task-item' + +type QuadrantCardProps = { + config: QuadrantConfig + tasks: Task[] + maxDisplay?: number +} + +const QuadrantCard: FC = ({ + config, + tasks, + maxDisplay = 5, +}) => { + const { title, subtitle, bgClass, borderClass, titleClass } = config + const displayTasks = tasks.slice(0, maxDisplay) + const remainingCount = Math.max(0, tasks.length - maxDisplay) + + return ( +
+ {/* Header */} +
+
{title}
+
{subtitle}
+
+ + {/* Task List */} +
+ {displayTasks.length > 0 + ? ( + displayTasks.map((task, index) => ( + + )) + ) + : ( +
+ No tasks +
+ )} +
+ + {/* More indicator */} + {remainingCount > 0 && ( +
+ + + {remainingCount} + {' '} + more +
+ )} +
+ ) +} + +export default QuadrantCard diff --git a/web/app/components/base/quadrant-matrix/task-item.tsx b/web/app/components/base/quadrant-matrix/task-item.tsx new file mode 100644 index 0000000000..5494e58511 --- /dev/null +++ b/web/app/components/base/quadrant-matrix/task-item.tsx @@ -0,0 +1,78 @@ +'use client' +import type { FC } from 'react' +import type { Task } from './types' +import { cn } from '@/utils/classnames' + +type ScoreBadgeProps = { + label: string + score: number + colorClass: string +} + +const ScoreBadge: FC = ({ label, score, colorClass }) => { + return ( + + {label} + : + {score} + + ) +} + +type TaskItemProps = { + task: Task + showScores?: boolean +} + +const TaskItem: FC = ({ task, showScores = true }) => { + const { name, description, deadline, importance_score, urgency_score, action_advice } = task + + return ( +
+ {/* Task Name */} +
{name}
+ + {/* Description (if exists) */} + {description && ( +
+ {description} +
+ )} + + {/* Metadata Row */} +
+ {/* Deadline Badge */} + {deadline && ( + + {deadline} + + )} + + {/* Scores (optional) */} + {showScores && ( +
+ + +
+ )} +
+ + {/* Action Advice (if exists) */} + {action_advice && ( +
+ {action_advice} +
+ )} +
+ ) +} + +export default TaskItem diff --git a/web/app/components/base/quadrant-matrix/types.ts b/web/app/components/base/quadrant-matrix/types.ts new file mode 100644 index 0000000000..044161aed1 --- /dev/null +++ b/web/app/components/base/quadrant-matrix/types.ts @@ -0,0 +1,79 @@ +/** + * Type definitions for Eisenhower Matrix (Task Quadrant) visualization + */ + +export type Task = { + name: string + description?: string + deadline?: string // YYYY-MM-DD format + importance_score: number // 0-100, based on goal alignment and long-term value + urgency_score: number // 0-100, based on deadline pressure and delay penalty + action_advice?: string // Suggested action for this task +} + +export type QuadrantData = { + q1: Task[] // Urgent & Important - Do First + q2: Task[] // Not Urgent & Important - Schedule + q3: Task[] // Urgent & Not Important - Delegate + q4: Task[] // Not Urgent & Not Important - Eliminate +} + +export type QuadrantConfig = { + key: 'q1' | 'q2' | 'q3' | 'q4' + title: string + subtitle: string + bgClass: string + borderClass: string + titleClass: string +} + +export const QUADRANT_CONFIGS: Record = { + q1: { + key: 'q1', + title: 'Do First', + subtitle: 'Urgent & Important', + bgClass: 'bg-state-destructive-hover', + borderClass: 'border-state-destructive-border', + titleClass: 'text-text-destructive', + }, + q2: { + key: 'q2', + title: 'Schedule', + subtitle: 'Important & Not Urgent', + bgClass: 'bg-state-accent-hover', + borderClass: 'border-state-accent-border', + titleClass: 'text-text-accent', + }, + q3: { + key: 'q3', + title: 'Delegate', + subtitle: 'Urgent & Not Important', + bgClass: 'bg-state-warning-hover', + borderClass: 'border-state-warning-border', + titleClass: 'text-text-warning', + }, + q4: { + key: 'q4', + title: 'Eliminate', + subtitle: 'Not Urgent & Not Important', + bgClass: 'bg-components-panel-on-panel-item-bg', + borderClass: 'border-divider-regular', + titleClass: 'text-text-tertiary', + }, +} + +/** + * Validates if the data structure matches QuadrantData interface + */ +export function isValidQuadrantData(data: unknown): data is QuadrantData { + if (typeof data !== 'object' || data === null) + return false + + const d = data as Record + return ( + Array.isArray(d.q1) + && Array.isArray(d.q2) + && Array.isArray(d.q3) + && Array.isArray(d.q4) + ) +}