mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
feat(web): add Eisenhower Matrix visualization component for task quadrants
Add a new quadrant-matrix component that renders tasks in a 2x2 grid based on importance and urgency scores. Integrate with code-block as a new 'quadrant' language type for markdown rendering.
This commit is contained in:
@ -16,6 +16,7 @@ import { Theme } from '@/types/app'
|
|||||||
import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory
|
import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory
|
||||||
|
|
||||||
const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false })
|
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
|
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
|
||||||
const capitalizationLanguageNameMap: Record<string, string> = {
|
const capitalizationLanguageNameMap: Record<string, string> = {
|
||||||
@ -40,6 +41,7 @@ const capitalizationLanguageNameMap: Record<string, string> = {
|
|||||||
latex: 'Latex',
|
latex: 'Latex',
|
||||||
svg: 'SVG',
|
svg: 'SVG',
|
||||||
abc: 'ABC',
|
abc: 'ABC',
|
||||||
|
quadrant: 'Quadrant',
|
||||||
}
|
}
|
||||||
const getCorrectCapitalizationLanguageName = (language: string) => {
|
const getCorrectCapitalizationLanguageName = (language: string) => {
|
||||||
if (!language)
|
if (!language)
|
||||||
@ -409,6 +411,12 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
|||||||
<MarkdownMusic children={content} />
|
<MarkdownMusic children={content} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
case 'quadrant':
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<QuadrantMatrix content={content} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
|
|||||||
140
web/app/components/base/quadrant-matrix/index.tsx
Normal file
140
web/app/components/base/quadrant-matrix/index.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import type { QuadrantData } from './types'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { cn } from '@/utils/classnames'
|
||||||
|
import QuadrantCard from './quadrant-card'
|
||||||
|
import { isValidQuadrantData, QUADRANT_CONFIGS } from './types'
|
||||||
|
|
||||||
|
type QuadrantMatrixProps = {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuadrantMatrix: FC<QuadrantMatrixProps> = ({ content }) => {
|
||||||
|
const parsedData = useMemo<QuadrantData | null>(() => {
|
||||||
|
try {
|
||||||
|
const trimmed = content.trim()
|
||||||
|
const data = JSON.parse(trimmed)
|
||||||
|
|
||||||
|
if (!isValidQuadrantData(data))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [content])
|
||||||
|
|
||||||
|
if (!parsedData) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center rounded-lg bg-components-panel-bg-blur p-8">
|
||||||
|
<div className="text-center text-text-secondary">
|
||||||
|
<div className="mb-2 text-lg">Invalid Quadrant Data</div>
|
||||||
|
<div className="text-sm text-text-tertiary">
|
||||||
|
Expected JSON format with q1, q2, q3, q4 arrays
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTasks
|
||||||
|
= parsedData.q1.length
|
||||||
|
+ parsedData.q2.length
|
||||||
|
+ parsedData.q3.length
|
||||||
|
+ parsedData.q4.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full rounded-lg bg-components-panel-bg-blur p-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="system-md-semibold text-text-primary">
|
||||||
|
Eisenhower Matrix
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-text-tertiary">
|
||||||
|
{totalTasks}
|
||||||
|
{' '}
|
||||||
|
task
|
||||||
|
{totalTasks !== 1 ? 's' : ''}
|
||||||
|
{' '}
|
||||||
|
across 4 quadrants
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-text-quaternary">
|
||||||
|
<span className="text-text-accent">I</span>
|
||||||
|
=Importance
|
||||||
|
<span className="ml-2 text-text-warning">U</span>
|
||||||
|
=Urgency
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Axis Labels */}
|
||||||
|
<div className="relative">
|
||||||
|
{/* Importance Label (Top Center) */}
|
||||||
|
<div className="mb-2 flex items-center justify-center">
|
||||||
|
<span className="rounded bg-state-accent-hover px-2 py-0.5 text-xs font-medium text-text-accent">
|
||||||
|
Important
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Grid with Urgency Labels */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{/* Left: Not Urgent Label */}
|
||||||
|
<div className="flex w-6 shrink-0 items-center justify-center">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'-rotate-90 whitespace-nowrap rounded px-2 py-0.5 text-xs font-medium',
|
||||||
|
'bg-components-panel-on-panel-item-bg text-text-tertiary',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Not Urgent
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center: 2x2 Grid */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{/* Row 1: Important */}
|
||||||
|
<QuadrantCard
|
||||||
|
config={QUADRANT_CONFIGS.q2}
|
||||||
|
tasks={parsedData.q2}
|
||||||
|
/>
|
||||||
|
<QuadrantCard
|
||||||
|
config={QUADRANT_CONFIGS.q1}
|
||||||
|
tasks={parsedData.q1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Row 2: Not Important */}
|
||||||
|
<QuadrantCard
|
||||||
|
config={QUADRANT_CONFIGS.q4}
|
||||||
|
tasks={parsedData.q4}
|
||||||
|
/>
|
||||||
|
<QuadrantCard
|
||||||
|
config={QUADRANT_CONFIGS.q3}
|
||||||
|
tasks={parsedData.q3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Urgent Label */}
|
||||||
|
<div className="flex w-6 shrink-0 items-center justify-center">
|
||||||
|
<span className="-rotate-90 whitespace-nowrap rounded bg-state-warning-hover px-2 py-0.5 text-xs font-medium text-text-warning">
|
||||||
|
Urgent
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Not Important Label (Bottom Center) */}
|
||||||
|
<div className="mt-2 flex items-center justify-center">
|
||||||
|
<span className="rounded bg-components-panel-on-panel-item-bg px-2 py-0.5 text-xs font-medium text-text-tertiary">
|
||||||
|
Not Important
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuadrantMatrix
|
||||||
64
web/app/components/base/quadrant-matrix/quadrant-card.tsx
Normal file
64
web/app/components/base/quadrant-matrix/quadrant-card.tsx
Normal file
@ -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<QuadrantCardProps> = ({
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex min-h-[180px] flex-col rounded-xl border p-3',
|
||||||
|
bgClass,
|
||||||
|
borderClass,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className={cn('system-sm-semibold', titleClass)}>{title}</div>
|
||||||
|
<div className="text-xs text-text-tertiary">{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task List */}
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
{displayTasks.length > 0
|
||||||
|
? (
|
||||||
|
displayTasks.map((task, index) => (
|
||||||
|
<TaskItem key={`${task.name}-${index}`} task={task} />
|
||||||
|
))
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className="flex flex-1 items-center justify-center text-xs text-text-quaternary">
|
||||||
|
No tasks
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* More indicator */}
|
||||||
|
{remainingCount > 0 && (
|
||||||
|
<div className="mt-2 text-center text-xs text-text-tertiary">
|
||||||
|
+
|
||||||
|
{remainingCount}
|
||||||
|
{' '}
|
||||||
|
more
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuadrantCard
|
||||||
78
web/app/components/base/quadrant-matrix/task-item.tsx
Normal file
78
web/app/components/base/quadrant-matrix/task-item.tsx
Normal file
@ -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<ScoreBadgeProps> = ({ label, score, colorClass }) => {
|
||||||
|
return (
|
||||||
|
<span className={cn('text-xs font-medium', colorClass)}>
|
||||||
|
{label}
|
||||||
|
:
|
||||||
|
{score}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskItemProps = {
|
||||||
|
task: Task
|
||||||
|
showScores?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskItem: FC<TaskItemProps> = ({ task, showScores = true }) => {
|
||||||
|
const { name, description, deadline, importance_score, urgency_score, action_advice } = task
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group rounded-lg bg-components-panel-bg p-2.5 shadow-xs transition-all hover:shadow-sm">
|
||||||
|
{/* Task Name */}
|
||||||
|
<div className="system-sm-medium text-text-primary">{name}</div>
|
||||||
|
|
||||||
|
{/* Description (if exists) */}
|
||||||
|
{description && (
|
||||||
|
<div className="mt-1 line-clamp-2 text-xs text-text-tertiary">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Metadata Row */}
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||||
|
{/* Deadline Badge */}
|
||||||
|
{deadline && (
|
||||||
|
<span className="bg-components-badge-bg-gray inline-flex items-center rounded px-1.5 py-0.5 text-xs text-text-tertiary">
|
||||||
|
{deadline}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Scores (optional) */}
|
||||||
|
{showScores && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ScoreBadge
|
||||||
|
label="I"
|
||||||
|
score={importance_score}
|
||||||
|
colorClass="text-text-accent"
|
||||||
|
/>
|
||||||
|
<ScoreBadge
|
||||||
|
label="U"
|
||||||
|
score={urgency_score}
|
||||||
|
colorClass="text-text-warning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Advice (if exists) */}
|
||||||
|
{action_advice && (
|
||||||
|
<div className="mt-2 border-t border-divider-subtle pt-2 text-xs italic text-text-quaternary">
|
||||||
|
{action_advice}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskItem
|
||||||
79
web/app/components/base/quadrant-matrix/types.ts
Normal file
79
web/app/components/base/quadrant-matrix/types.ts
Normal file
@ -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<string, QuadrantConfig> = {
|
||||||
|
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<string, unknown>
|
||||||
|
return (
|
||||||
|
Array.isArray(d.q1)
|
||||||
|
&& Array.isArray(d.q2)
|
||||||
|
&& Array.isArray(d.q3)
|
||||||
|
&& Array.isArray(d.q4)
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user