mirror of
https://github.com/langgenius/dify.git
synced 2026-01-19 13:40:07 +00:00
Compare commits
6 Commits
feat/stora
...
feature/ta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0aa8cdb45 | ||
|
|
ae8618877b | ||
|
|
1c55602445 | ||
|
|
a3f1220d23 | ||
|
|
d62e16b9bb | ||
|
|
13f2a43ccc |
@@ -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<string, string> = {
|
||||
@@ -40,6 +41,7 @@ const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
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
|
||||
<MarkdownMusic children={content} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
case 'quadrant':
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<QuadrantMatrix content={content} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
|
||||
153
web/app/components/base/quadrant-matrix/index.tsx
Normal file
153
web/app/components/base/quadrant-matrix/index.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { QuadrantData } from './types'
|
||||
import { RiExpandDiagonalLine } from '@remixicon/react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
||||
import QuadrantCard from './quadrant-card'
|
||||
import { isValidQuadrantData, QUADRANT_CONFIGS } from './types'
|
||||
|
||||
type QuadrantMatrixProps = {
|
||||
content: string
|
||||
}
|
||||
|
||||
const QuadrantMatrix: FC<QuadrantMatrixProps> = ({ content }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
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])
|
||||
|
||||
const handleExpand = useCallback(() => {
|
||||
setIsExpanded(true)
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsExpanded(false)
|
||||
}, [])
|
||||
|
||||
if (!parsedData) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-xl bg-components-panel-bg-blur p-8">
|
||||
<div className="text-center text-text-secondary">
|
||||
<div className="system-md-semibold mb-2">{t('quadrantMatrix.invalidData', { ns: 'app' })}</div>
|
||||
<div className="text-sm text-text-tertiary">
|
||||
{t('quadrantMatrix.invalidDataDesc', { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const totalTasks
|
||||
= parsedData.q1.length
|
||||
+ parsedData.q2.length
|
||||
+ parsedData.q3.length
|
||||
+ parsedData.q4.length
|
||||
|
||||
// Shared grid content component
|
||||
const renderGrid = (expanded: boolean) => (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Row 1: Q1 (Do First), Q2 (Schedule) */}
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q1}
|
||||
tasks={parsedData.q1}
|
||||
expanded={expanded}
|
||||
/>
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q2}
|
||||
tasks={parsedData.q2}
|
||||
expanded={expanded}
|
||||
/>
|
||||
|
||||
{/* Row 2: Q3 (Delegate), Q4 (Don't Do) */}
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q3}
|
||||
tasks={parsedData.q3}
|
||||
expanded={expanded}
|
||||
/>
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q4}
|
||||
tasks={parsedData.q4}
|
||||
expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full overflow-hidden rounded-xl 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">
|
||||
{t('quadrantMatrix.title', { ns: 'app' })}
|
||||
</div>
|
||||
<div className="text-xs text-text-tertiary">
|
||||
{t('quadrantMatrix.taskCount', { ns: 'app', count: totalTasks })}
|
||||
</div>
|
||||
</div>
|
||||
{/* Legend + Expand Button */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 text-[11px] text-text-quaternary">
|
||||
<span>{t('quadrantMatrix.legend.importance', { ns: 'app' })}</span>
|
||||
<span>{t('quadrantMatrix.legend.urgency', { ns: 'app' })}</span>
|
||||
</div>
|
||||
<ActionButton onClick={handleExpand}>
|
||||
<RiExpandDiagonalLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2x2 Grid */}
|
||||
{renderGrid(false)}
|
||||
</div>
|
||||
|
||||
{/* Fullscreen Modal */}
|
||||
<FullScreenModal
|
||||
open={isExpanded}
|
||||
onClose={handleClose}
|
||||
closable
|
||||
>
|
||||
<div className="flex h-full flex-col p-6">
|
||||
{/* Modal Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xl font-semibold text-text-primary">
|
||||
{t('quadrantMatrix.title', { ns: 'app' })}
|
||||
</div>
|
||||
<div className="text-sm text-text-tertiary">
|
||||
{t('quadrantMatrix.taskCount', { ns: 'app', count: totalTasks })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-text-quaternary">
|
||||
<span>{t('quadrantMatrix.legend.importance', { ns: 'app' })}</span>
|
||||
<span>{t('quadrantMatrix.legend.urgency', { ns: 'app' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Grid */}
|
||||
<div className="min-h-0 flex-1">
|
||||
{renderGrid(true)}
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuadrantMatrix
|
||||
102
web/app/components/base/quadrant-matrix/quadrant-card.tsx
Normal file
102
web/app/components/base/quadrant-matrix/quadrant-card.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { QuadrantConfig, Task } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import TaskItem from './task-item'
|
||||
|
||||
type QuadrantCardProps = {
|
||||
config: QuadrantConfig
|
||||
tasks: Task[]
|
||||
expanded?: boolean
|
||||
maxDisplay?: number
|
||||
}
|
||||
|
||||
const QuadrantCard: FC<QuadrantCardProps> = ({
|
||||
config,
|
||||
tasks,
|
||||
expanded = false,
|
||||
maxDisplay = 3,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { number, titleKey, subtitleKey, bgClass, borderClass, titleClass } = config
|
||||
const displayLimit = expanded ? Infinity : maxDisplay
|
||||
const displayTasks = tasks.slice(0, displayLimit)
|
||||
const remainingCount = Math.max(0, tasks.length - displayLimit)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-w-0 flex-col rounded-xl border p-3',
|
||||
bgClass,
|
||||
borderClass,
|
||||
expanded ? 'min-h-[280px]' : 'min-h-[200px]',
|
||||
)}
|
||||
>
|
||||
{/* Header with numbered circle */}
|
||||
<div className="mb-2 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Numbered circle */}
|
||||
<span className={cn(
|
||||
'flex h-5 w-5 items-center justify-center rounded-full border text-xs font-semibold',
|
||||
borderClass,
|
||||
titleClass,
|
||||
)}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
<span className={cn('system-sm-semibold', titleClass)}>{t(titleKey, { ns: 'app' })}</span>
|
||||
{tasks.length > 0 && (
|
||||
<span className="bg-components-badge-bg-gray rounded-full px-1.5 py-0.5 text-[10px] font-medium text-text-tertiary">
|
||||
{tasks.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-text-tertiary">{t(subtitleKey, { ns: 'app' })}</div>
|
||||
</div>
|
||||
|
||||
{/* Task List */}
|
||||
<div className={cn(
|
||||
'flex min-h-0 flex-1 flex-col gap-2',
|
||||
expanded && 'overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
{displayTasks.length > 0
|
||||
? (
|
||||
displayTasks.map((task) => {
|
||||
const taskKey = [
|
||||
task.name,
|
||||
task.deadline ?? 'no-deadline',
|
||||
task.importance_score,
|
||||
task.urgency_score,
|
||||
task.description ?? '',
|
||||
task.action_advice ?? '',
|
||||
].join('|')
|
||||
|
||||
return (
|
||||
<TaskItem
|
||||
key={taskKey}
|
||||
task={task}
|
||||
expanded={expanded}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-1 items-center justify-center text-xs text-text-quaternary">
|
||||
{t('quadrantMatrix.noTasks', { ns: 'app' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* More indicator (only in non-expanded mode) */}
|
||||
{!expanded && remainingCount > 0 && (
|
||||
<div className="mt-2 shrink-0 text-center text-[11px] text-text-tertiary">
|
||||
{t('quadrantMatrix.more', { ns: 'app', count: remainingCount })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuadrantCard
|
||||
88
web/app/components/base/quadrant-matrix/task-item.tsx
Normal file
88
web/app/components/base/quadrant-matrix/task-item.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Task } from './types'
|
||||
import { RiCalendarLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type TaskItemProps = {
|
||||
task: Task
|
||||
expanded?: boolean
|
||||
showScores?: boolean
|
||||
}
|
||||
|
||||
const TaskItem: FC<TaskItemProps> = ({ task, expanded = false, showScores = true }) => {
|
||||
const { t } = useTranslation()
|
||||
const { name, description, deadline, importance_score, urgency_score, action_advice } = task
|
||||
|
||||
return (
|
||||
<div className="group min-w-0 rounded-lg bg-components-panel-bg p-2.5 shadow-xs transition-all hover:shadow-sm">
|
||||
{/* Header: Task Name + Scores */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-medium min-w-0 flex-1 text-text-primary',
|
||||
!expanded && 'truncate',
|
||||
)}
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
{showScores && (
|
||||
<div className="flex shrink-0 items-center gap-1 text-[10px] font-medium">
|
||||
<span className="text-text-accent">
|
||||
I:
|
||||
{importance_score}
|
||||
</span>
|
||||
<span className="text-text-warning">
|
||||
U:
|
||||
{urgency_score}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<div className={cn(
|
||||
'mt-1 text-xs text-text-tertiary',
|
||||
!expanded && 'line-clamp-2',
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deadline Badge */}
|
||||
{deadline && (
|
||||
<div className="mt-1.5">
|
||||
<span className="bg-components-badge-bg-gray inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-text-tertiary">
|
||||
<RiCalendarLine className="h-3 w-3" />
|
||||
<span>
|
||||
{t('quadrantMatrix.deadline', { ns: 'app' })}
|
||||
{' '}
|
||||
{deadline}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Advice */}
|
||||
{action_advice && (
|
||||
<div className="mt-2 border-t border-divider-subtle pt-2">
|
||||
<p
|
||||
className={cn(
|
||||
'text-xs italic text-text-quaternary',
|
||||
!expanded && 'line-clamp-2',
|
||||
)}
|
||||
title={!expanded ? action_advice : undefined}
|
||||
>
|
||||
{action_advice}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskItem
|
||||
92
web/app/components/base/quadrant-matrix/types.ts
Normal file
92
web/app/components/base/quadrant-matrix/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Type definitions for Eisenhower Matrix (Task Quadrant) visualization
|
||||
*/
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
|
||||
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 - Don't Do
|
||||
}
|
||||
|
||||
type QuadrantKeyBase = I18nKeysWithPrefix<'app', 'quadrantMatrix.q'>
|
||||
type QuadrantTitleKey = Extract<QuadrantKeyBase, `${string}.title`>
|
||||
type QuadrantSubtitleKey = Extract<QuadrantKeyBase, `${string}.subtitle`>
|
||||
|
||||
export type QuadrantConfig = {
|
||||
key: 'q1' | 'q2' | 'q3' | 'q4'
|
||||
number: number
|
||||
titleKey: QuadrantTitleKey // i18n key for title
|
||||
subtitleKey: QuadrantSubtitleKey // i18n key for subtitle
|
||||
bgClass: string
|
||||
borderClass: string
|
||||
titleClass: string
|
||||
}
|
||||
|
||||
// Layout based on Eisenhower Matrix:
|
||||
// Q1 (Do First) - top-left, Q2 (Schedule) - top-right
|
||||
// Q3 (Delegate) - bottom-left, Q4 (Don't Do) - bottom-right
|
||||
export const QUADRANT_CONFIGS: Record<string, QuadrantConfig> = {
|
||||
q1: {
|
||||
key: 'q1',
|
||||
number: 1,
|
||||
titleKey: 'quadrantMatrix.q1.title',
|
||||
subtitleKey: 'quadrantMatrix.q1.subtitle',
|
||||
bgClass: 'bg-state-destructive-hover',
|
||||
borderClass: 'border-state-destructive-border',
|
||||
titleClass: 'text-text-destructive',
|
||||
},
|
||||
q2: {
|
||||
key: 'q2',
|
||||
number: 2,
|
||||
titleKey: 'quadrantMatrix.q2.title',
|
||||
subtitleKey: 'quadrantMatrix.q2.subtitle',
|
||||
bgClass: 'bg-state-accent-hover',
|
||||
borderClass: 'border-state-accent-border',
|
||||
titleClass: 'text-text-accent',
|
||||
},
|
||||
q3: {
|
||||
key: 'q3',
|
||||
number: 3,
|
||||
titleKey: 'quadrantMatrix.q3.title',
|
||||
subtitleKey: 'quadrantMatrix.q3.subtitle',
|
||||
bgClass: 'bg-state-warning-hover',
|
||||
borderClass: 'border-state-warning-border',
|
||||
titleClass: 'text-text-warning',
|
||||
},
|
||||
q4: {
|
||||
key: 'q4',
|
||||
number: 4,
|
||||
titleKey: 'quadrantMatrix.q4.title',
|
||||
subtitleKey: 'quadrantMatrix.q4.subtitle',
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -196,6 +196,24 @@
|
||||
"publishApp.notSet": "Not set",
|
||||
"publishApp.notSetDesc": "Currently nobody can access the web app. Please set permissions.",
|
||||
"publishApp.title": "Who can access web app",
|
||||
"quadrantMatrix.deadline": "DDL:",
|
||||
"quadrantMatrix.invalidData": "Invalid Quadrant Data",
|
||||
"quadrantMatrix.invalidDataDesc": "Expected JSON format with q1, q2, q3, q4 arrays",
|
||||
"quadrantMatrix.legend.importance": "I = Importance",
|
||||
"quadrantMatrix.legend.urgency": "U = Urgency",
|
||||
"quadrantMatrix.more": "+{{count}} more",
|
||||
"quadrantMatrix.noTasks": "No tasks",
|
||||
"quadrantMatrix.q1.subtitle": "Urgent & Important",
|
||||
"quadrantMatrix.q1.title": "Do First",
|
||||
"quadrantMatrix.q2.subtitle": "Important & Not Urgent",
|
||||
"quadrantMatrix.q2.title": "Schedule",
|
||||
"quadrantMatrix.q3.subtitle": "Urgent & Not Important",
|
||||
"quadrantMatrix.q3.title": "Delegate",
|
||||
"quadrantMatrix.q4.subtitle": "Not Urgent & Not Important",
|
||||
"quadrantMatrix.q4.title": "Don't Do",
|
||||
"quadrantMatrix.taskCount_one": "{{count}} task prioritized",
|
||||
"quadrantMatrix.taskCount_other": "{{count}} tasks prioritized",
|
||||
"quadrantMatrix.title": "Eisenhower Matrix",
|
||||
"removeOriginal": "Delete the original app",
|
||||
"roadmap": "See our roadmap",
|
||||
"showMyCreatedAppsOnly": "Created by me",
|
||||
|
||||
@@ -196,6 +196,24 @@
|
||||
"publishApp.notSet": "未设置",
|
||||
"publishApp.notSetDesc": "当前任何人都无法访问 Web 应用。请设置访问权限。",
|
||||
"publishApp.title": "谁可以访问 web 应用",
|
||||
"quadrantMatrix.deadline": "截止:",
|
||||
"quadrantMatrix.invalidData": "无效的象限数据",
|
||||
"quadrantMatrix.invalidDataDesc": "需要包含 q1, q2, q3, q4 数组的 JSON 格式",
|
||||
"quadrantMatrix.legend.importance": "I = 重要性",
|
||||
"quadrantMatrix.legend.urgency": "U = 紧急性",
|
||||
"quadrantMatrix.more": "+{{count}} 更多",
|
||||
"quadrantMatrix.noTasks": "暂无任务",
|
||||
"quadrantMatrix.q1.subtitle": "紧急且重要",
|
||||
"quadrantMatrix.q1.title": "立即执行",
|
||||
"quadrantMatrix.q2.subtitle": "重要但不紧急",
|
||||
"quadrantMatrix.q2.title": "计划安排",
|
||||
"quadrantMatrix.q3.subtitle": "紧急但不重要",
|
||||
"quadrantMatrix.q3.title": "委派他人",
|
||||
"quadrantMatrix.q4.subtitle": "不紧急也不重要",
|
||||
"quadrantMatrix.q4.title": "不要做",
|
||||
"quadrantMatrix.taskCount_one": "{{count}} 个任务已排序",
|
||||
"quadrantMatrix.taskCount_other": "{{count}} 个任务已排序",
|
||||
"quadrantMatrix.title": "艾森豪威尔矩阵",
|
||||
"removeOriginal": "删除原应用",
|
||||
"roadmap": "产品路线图",
|
||||
"showMyCreatedAppsOnly": "我创建的",
|
||||
|
||||
Reference in New Issue
Block a user