466 lines
20 KiB
TypeScript
466 lines
20 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { ArrowRight, Bot, CircleCheck, Inbox, Layers, Loader, Plus, Search, Users, X } from 'lucide-react';
|
|
import dayjs from 'dayjs';
|
|
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from 'tdesign-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTheme } from '@/contexts/ThemeContext';
|
|
import { requestCrudService } from '@/service/request-axios-service';
|
|
import { toast } from 'sonner';
|
|
import { KanbanColumn } from './KanbanColumn';
|
|
import { TaskListItem } from './TaskManagement';
|
|
import { listBusinessMappings } from '@/strategy-components/api/business-mapping';
|
|
import { readRuntimeEnv } from '@/env';
|
|
|
|
interface TaskListPageProps {
|
|
title: string;
|
|
category: string;
|
|
subCategory: string;
|
|
}
|
|
|
|
interface StateItem {
|
|
startTime: string;
|
|
endTime: string;
|
|
task: TaskListItem | null;
|
|
}
|
|
|
|
type ColumnType = 'unanalyzed' | 'running' | 'completed';
|
|
|
|
const service = requestCrudService;
|
|
const zncmUrl = readRuntimeEnv('VITE_API_BASE_URL') || '';
|
|
/** 本页对应的业务码(3QFX = 3Q);用于查业务映射拿到要自动选用的业务链 / 单智能体。 */
|
|
const BUSINESS_CODE = '3Q';
|
|
|
|
const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory }) => {
|
|
const { theme } = useTheme();
|
|
const navigate = useNavigate();
|
|
const isDark = theme === 'dark';
|
|
const isLightBlue = theme === 'lightblue';
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
|
|
const [selectedTask, setSelectedTask] = useState<TaskListItem | null>(null);
|
|
const [unanalyzedTotal, setUnanalyzedTotal] = useState(0);
|
|
const [runningTotal, setRunningTotal] = useState(0);
|
|
const [completedTotal, setCompletedTotal] = useState(0);
|
|
const [unanalyzedList, setUnanalyzedList] = useState<TaskListItem[]>([]);
|
|
const [runningList, setRunningList] = useState<TaskListItem[]>([]);
|
|
const [completedList, setCompletedList] = useState<TaskListItem[]>([]);
|
|
const [unanalyzedLoading, setUnanalyzedLoading] = useState(false);
|
|
const [runningLoading, setRunningLoading] = useState(false);
|
|
const [completedLoading, setCompletedLoading] = useState(false);
|
|
const [range, setRange] = useState<DateRangeValue>([]);
|
|
|
|
const stateRef = useRef<StateItem>({
|
|
startTime: '',
|
|
endTime: '',
|
|
task: null,
|
|
});
|
|
|
|
const presets = useMemo<DateRangePickerProps['presets']>(
|
|
() => ({
|
|
最近7天: [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
|
|
最近3天: [dayjs().subtract(2, 'day').toDate(), dayjs().toDate()],
|
|
今天: [dayjs().toDate(), dayjs().toDate()],
|
|
}),
|
|
[],
|
|
);
|
|
|
|
const taskStatusMap: Record<ColumnType, number[]> = {
|
|
unanalyzed: [0, 10],
|
|
running: [20],
|
|
completed: [25, 30, 40, 60, 70, 80, 90, 100, 110],
|
|
};
|
|
|
|
const setRangeChange = async (value: DateRangeValue) => {
|
|
setRange(value);
|
|
stateRef.current.startTime = '';
|
|
stateRef.current.endTime = '';
|
|
|
|
if (Array.isArray(value) && value.length > 1) {
|
|
stateRef.current.startTime = String(value[0] || '');
|
|
stateRef.current.endTime = String(value[1] || '');
|
|
}
|
|
|
|
await taskDataChange();
|
|
};
|
|
|
|
const taskListSearch = async (
|
|
taskType: ColumnType,
|
|
pageNum = 1,
|
|
pageSize = 10,
|
|
) => {
|
|
if (taskType === 'unanalyzed') setUnanalyzedLoading(true);
|
|
if (taskType === 'running') setRunningLoading(true);
|
|
if (taskType === 'completed') setCompletedLoading(true);
|
|
|
|
try {
|
|
const response = await service.post(`${zncmUrl}/three/cop-three-tasks`, {
|
|
content: searchQuery,
|
|
pageNum,
|
|
pageSize,
|
|
taskStatus: taskStatusMap[taskType],
|
|
startTime: stateRef.current.startTime,
|
|
endTime: stateRef.current.endTime,
|
|
taskDirection: '',
|
|
});
|
|
|
|
const result = response?.data ?? {};
|
|
const records = Array.isArray(result.records) ? result.records : [];
|
|
const normalized = records.map((item: TaskListItem) => ({
|
|
...item,
|
|
column: taskType,
|
|
taskLevel: item.taskLevel || 1,
|
|
}));
|
|
const total = Number(result.total ?? normalized.length);
|
|
|
|
if (taskType === 'unanalyzed') {
|
|
setUnanalyzedTotal(total);
|
|
setUnanalyzedList(normalized);
|
|
} else if (taskType === 'running') {
|
|
setRunningTotal(total);
|
|
setRunningList(normalized);
|
|
} else {
|
|
setCompletedTotal(total);
|
|
setCompletedList(normalized);
|
|
}
|
|
} finally {
|
|
if (taskType === 'unanalyzed') setUnanalyzedLoading(false);
|
|
if (taskType === 'running') setRunningLoading(false);
|
|
if (taskType === 'completed') setCompletedLoading(false);
|
|
}
|
|
};
|
|
|
|
const pageIndexChange = async (type: string, pageIndex: number) => {
|
|
await taskListSearch(type as ColumnType, pageIndex);
|
|
};
|
|
|
|
const taskDataChange = async (pageNum = 1, pageSize = 10) => {
|
|
await Promise.all([
|
|
taskListSearch('unanalyzed', pageNum, pageSize),
|
|
taskListSearch('running', pageNum, pageSize),
|
|
taskListSearch('completed', pageNum, pageSize),
|
|
]);
|
|
};
|
|
|
|
const handleStartAnalysis = (task: TaskListItem) => {
|
|
setSelectedTask(task);
|
|
stateRef.current.task = task;
|
|
setShowAnalysisModal(true);
|
|
};
|
|
|
|
const handleOpenTaskDetail = () => {
|
|
if (!stateRef.current.task) return;
|
|
window.open(`https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-info-search?id=${stateRef.current.task.id}`);
|
|
setShowAnalysisModal(false);
|
|
};
|
|
|
|
const handleOpenConsultation = () => {
|
|
const task = stateRef.current.task;
|
|
if (!task) return;
|
|
// 会商自动化串联(模块 C):跳转圆桌规划页,带上任务名称/描述 + 本页业务码(3Q=三情分析)。
|
|
// 圆桌据此自动开跑 Step 1,并在「进入研讨」时自动选用 3Q 配置的业务链(跳过选链弹窗)。
|
|
navigate('/page/strategy/qa/multi-agent-roundtable', {
|
|
state: {
|
|
roundtablePrefill: {
|
|
taskTitle: task.taskName,
|
|
taskGoal: task.overview,
|
|
businessCode: BUSINESS_CODE,
|
|
autoStart: true,
|
|
},
|
|
},
|
|
});
|
|
setShowAnalysisModal(false);
|
|
};
|
|
|
|
// 单智能体分析:用「配置管理」里为本业务配置的智能体,跳转到其问答页(新会话),
|
|
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
|
|
// 自动发出并开始问答。未配置智能体则提示去配置。
|
|
const handleSingleAgentAnalysis = async () => {
|
|
const task = stateRef.current.task;
|
|
if (!task) return;
|
|
let agentId: string | null = null;
|
|
try {
|
|
const mappings = await listBusinessMappings();
|
|
agentId = mappings.find((m) => m.businessCode === BUSINESS_CODE)?.agentId ?? null;
|
|
} catch {
|
|
/* ignore → 下面按未配置处理 */
|
|
}
|
|
if (!agentId) {
|
|
toast.error('该业务尚未在「配置管理」中配置单智能体');
|
|
return;
|
|
}
|
|
const text = [
|
|
task.taskName ? `任务名称:${task.taskName}` : '',
|
|
task.overview ? `任务简介:${task.overview}` : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
try {
|
|
sessionStorage.setItem(
|
|
'pendingAgentMessage',
|
|
JSON.stringify({ agentId, text }),
|
|
);
|
|
} catch {
|
|
/* noop */
|
|
}
|
|
navigate(`/page/strategy/agents/${agentId}/chats/new`);
|
|
setShowAnalysisModal(false);
|
|
};
|
|
|
|
const handleKeyDown = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
if ((event.metaKey || event.ctrlKey || event.shiftKey) && event.key === 'Enter') {
|
|
event.preventDefault();
|
|
} else if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
await taskDataChange();
|
|
} else if (event.key === '/') {
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
taskDataChange();
|
|
}, []);
|
|
|
|
const shellClass = isDark
|
|
? 'strategy-data h-full overflow-y-auto'
|
|
: isLightBlue
|
|
? 'strategy-data h-full overflow-y-auto bg-[linear-gradient(180deg,#F9FCFF_0%,#F4F9FF_46%,#FAFAFA_100%)]'
|
|
: 'strategy-data h-full overflow-y-auto bg-[linear-gradient(180deg,#FFFDFD_0%,#FFF7F7_46%,#FAFAFA_100%)]';
|
|
|
|
const toolbarClass = isDark
|
|
? 'border border-[rgba(43,143,254,0.55)] bg-[rgba(3,38,80,0.82)] text-white shadow-[0_16px_36px_rgba(3,10,22,0.2)]'
|
|
: 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_2px_8px_rgba(34,34,34,0.08)]';
|
|
|
|
const metricClass = isDark
|
|
? 'border border-[rgba(43,143,254,0.55)] bg-[rgba(3,38,80,0.82)]'
|
|
: 'border border-[#E8E8E8] bg-white';
|
|
|
|
const inputClass = isDark
|
|
? 'border-[rgba(67,89,124,0.46)] bg-[rgba(8,19,35,0.9)] text-white placeholder:text-slate-500'
|
|
: 'border-[#E8E8E8] bg-white text-[#222222] placeholder:text-[#A6A6A6]';
|
|
|
|
const metrics = [
|
|
{ label: '全部任务', value: unanalyzedTotal + runningTotal + completedTotal, icon: Layers, fg: '#222222', chipBg: '#F5F5F5', chipFg: '#7F7F7F' },
|
|
{ label: '未分析', value: unanalyzedTotal, icon: Inbox, fg: isLightBlue ? '#0062D9' : '#F5222D', chipBg: isLightBlue ? '#EAF4FF' : '#FEEDED', chipFg: isLightBlue ? '#0062D9' : '#F5222D' },
|
|
{ label: '分析中', value: runningTotal, icon: Loader, fg: isLightBlue ? '#0079FE' : '#1677FF', chipBg: isLightBlue ? '#F4F9FF' : '#EAF4FF', chipFg: isLightBlue ? '#0079FE' : '#1677FF' },
|
|
{ label: '已完成', value: completedTotal, icon: CircleCheck, fg: '#52C41A', chipBg: '#EEFCF7', chipFg: '#189D72' },
|
|
];
|
|
|
|
return (
|
|
<div className={shellClass}>
|
|
<div className="mx-auto flex min-h-full max-w-[1680px] flex-col gap-4 px-6 py-6 lg:px-8">
|
|
<section className={`rounded px-6 py-5 ${toolbarClass}`}>
|
|
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
|
<div className="space-y-2.5">
|
|
<div className="flex items-center gap-1.5 text-xs">
|
|
<span className={`rounded-sm border px-2 py-0.5 font-medium ${isDark ? 'border-[rgba(108,161,255,0.24)] bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : isLightBlue ? 'border-[#AAD2FF] bg-[#EAF4FF] text-[#0062D9]' : 'border-[#FFD6D6] bg-[#FFF1F0] text-[#CF1322]'}`}>
|
|
{category}
|
|
</span>
|
|
<ArrowRight className={`h-3 w-3 ${isDark ? 'text-slate-500' : 'text-[#A6A6A6]'}`} />
|
|
<span className={`rounded-sm border px-2 py-0.5 font-medium ${isDark ? 'border-[rgba(103,232,165,0.2)] bg-[rgba(34,197,94,0.14)] text-[#89E8B8]' : isLightBlue ? 'border-[#C1EBFB] bg-[#EFFAFE] text-[#2F9ACC]' : 'border-[#FFE2B8] bg-[#FFF6E8] text-[#D46B08]'}`}>
|
|
{subCategory}
|
|
</span>
|
|
</div>
|
|
|
|
<div>
|
|
<h1 className={`text-[24px] font-semibold leading-8 ${isDark ? 'text-white' : 'text-[#222222]'}`}>{title}</h1>
|
|
<p className={`mt-1.5 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
|
|
面向规划任务的情绪与倾向分析看板,支持快速检索、会商启动与结果回看。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3 xl:items-end">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<label className={`flex h-10 min-w-[300px] items-center gap-2 rounded-sm border px-3 ${inputClass}`}>
|
|
<Search className={`h-4 w-4 ${isDark ? 'text-slate-400' : 'text-[#A6A6A6]'}`} />
|
|
<input
|
|
value={searchQuery}
|
|
onChange={(event) => setSearchQuery(event.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
className="w-full bg-transparent text-sm outline-none"
|
|
placeholder="搜索任务名称"
|
|
/>
|
|
</label>
|
|
<DateRangePicker
|
|
className="sentiment-date-range"
|
|
size="large"
|
|
value={range}
|
|
presets={presets}
|
|
onChange={setRangeChange}
|
|
enableTimePicker
|
|
clearable
|
|
popupProps={{ overlayClassName: 'sentiment-date-range-popup' }}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className={`inline-flex items-center gap-2 rounded-sm px-4 py-2 text-sm ${isDark ? 'bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]' : isLightBlue ? 'bg-[#0062D9] text-white hover:bg-[#0079FE]' : 'bg-[#F5222D] text-white hover:bg-[#CF1322]'}`}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
新建任务
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
{metrics.map((item) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<div key={item.label} className={`rounded p-4 ${metricClass}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-[#7F7F7F]'}`}>{item.label}</p>
|
|
<p
|
|
className={`mt-2 text-[30px] font-semibold leading-none ${isDark ? 'text-white' : ''}`}
|
|
style={isDark ? undefined : { color: item.fg }}
|
|
>
|
|
{item.value}
|
|
</p>
|
|
</div>
|
|
<div
|
|
className={`flex h-11 w-11 items-center justify-center rounded ${isDark ? 'bg-[rgba(77,141,255,0.18)] text-[#9CCBFF]' : ''}`}
|
|
style={isDark ? undefined : { backgroundColor: item.chipBg, color: item.chipFg }}
|
|
>
|
|
<Icon className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
|
|
<section className="grid grid-cols-1 items-stretch gap-4 xl:grid-cols-3">
|
|
<div className="h-full">
|
|
<Loading className="block h-full" loading={unanalyzedLoading} showOverlay={false}>
|
|
<div className="h-full">
|
|
<KanbanColumn
|
|
title="未分析"
|
|
count={unanalyzedTotal}
|
|
color={isDark ? 'bg-[#7CC9FF]' : isLightBlue ? 'bg-[#0062D9]' : 'bg-[#F5222D]'}
|
|
tasks={unanalyzedList}
|
|
taskType="unanalyzed"
|
|
taskDataType="sentiment"
|
|
pageIndexSearch={pageIndexChange}
|
|
handleStartAnalysis={handleStartAnalysis}
|
|
/>
|
|
</div>
|
|
</Loading>
|
|
</div>
|
|
|
|
<div className="h-full">
|
|
<Loading className="block h-full" loading={runningLoading} showOverlay={false}>
|
|
<div className="h-full">
|
|
<KanbanColumn
|
|
title="分析中"
|
|
count={runningTotal}
|
|
color={isDark ? 'bg-[#5EEAD4]' : 'bg-[#1677FF]'}
|
|
tasks={runningList}
|
|
taskType="running"
|
|
taskDataType="sentiment"
|
|
pageIndexSearch={pageIndexChange}
|
|
handleStartAnalysis={handleStartAnalysis}
|
|
/>
|
|
</div>
|
|
</Loading>
|
|
</div>
|
|
|
|
<div className="h-full">
|
|
<Loading className="block h-full" loading={completedLoading} showOverlay={false}>
|
|
<div className="h-full">
|
|
<KanbanColumn
|
|
title="已完成"
|
|
count={completedTotal}
|
|
color={isDark ? 'bg-[#67E8A5]' : 'bg-[#52C41A]'}
|
|
tasks={completedList}
|
|
taskType="completed"
|
|
taskDataType="sentiment"
|
|
pageIndexSearch={pageIndexChange}
|
|
handleStartAnalysis={handleStartAnalysis}
|
|
/>
|
|
</div>
|
|
</Loading>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{showAnalysisModal && selectedTask ? (
|
|
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-black/45 px-4">
|
|
<div className={`w-full max-w-[560px] rounded border p-6 ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(10,23,42,0.98)] text-white' : 'border-[#E8E8E8] bg-white text-[#222222] shadow-[0_8px_32px_rgba(34,34,34,0.16)]'}`}>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h3 className="text-xl font-semibold">选择分析模式</h3>
|
|
<p className={`mt-2 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
|
|
为任务“{selectedTask.taskName}”选择分析方式
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAnalysisModal(false)}
|
|
className={`rounded-sm p-2 ${isDark ? 'hover:bg-[rgba(21,42,72,0.9)]' : isLightBlue ? 'hover:bg-[#EAF4FF]' : 'hover:bg-[#FFF1F0]'}`}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-6 space-y-3">
|
|
<button
|
|
type="button"
|
|
onClick={handleOpenTaskDetail}
|
|
className={`flex w-full items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(110,168,255,0.46)]' : isLightBlue ? 'border-[#E8E8E8] bg-[#F4F9FF] hover:border-[#80BCFF]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
|
|
>
|
|
<div>
|
|
<div className="font-medium">查看任务详情</div>
|
|
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
|
|
进入 B1 系统查看任务详情。
|
|
</div>
|
|
</div>
|
|
<ArrowRight className="h-4 w-4 opacity-70" />
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleOpenConsultation}
|
|
className={`flex w-full items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(103,232,165,0.42)]' : isLightBlue ? 'border-[#E8E8E8] bg-[#F4F9FF] hover:border-[#80BCFF]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<Users className={`mt-0.5 h-5 w-5 ${isDark ? 'text-[#89E8B8]' : isLightBlue ? 'text-[#0062D9]' : 'text-[#CF1322]'}`} />
|
|
<div>
|
|
<div className="font-medium">多智能体会商</div>
|
|
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
|
|
多个 AI 智能体从不同视角开展讨论并形成共识。
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ArrowRight className="h-4 w-4 opacity-70" />
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleSingleAgentAnalysis}
|
|
className={`flex w-full items-center justify-between rounded border px-5 py-4 text-left transition ${isDark ? 'border-[rgba(67,89,124,0.46)] bg-[rgba(15,33,60,0.92)] hover:border-[rgba(167,139,250,0.46)]' : isLightBlue ? 'border-[#E8E8E8] bg-[#F4F9FF] hover:border-[#80BCFF]' : 'border-[#E8E8E8] bg-[#FFF9F9] hover:border-[#F6B0B4]'}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<Bot className={`mt-0.5 h-5 w-5 ${isDark ? 'text-[#C4B5FD]' : 'text-[#7C3AED]'}`} />
|
|
<div>
|
|
<div className="font-medium">单智能体分析</div>
|
|
<div className={`mt-1 text-sm ${isDark ? 'text-slate-300' : 'text-[#575757]'}`}>
|
|
由单个 AI 智能体直接对该任务进行分析问答。
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ArrowRight className="h-4 w-4 opacity-70" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SentimentAnalyzes;
|