/
sweetebin
/
hackaton_26
Обзор
Документация
Войти
/
sweetebin
/
hackaton_26
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
web-form-project/src/components/TasksPanel.jsx
241 строка
9 KB
sweetebin
Трассировка до первоисточника в UI + CLI-инструментарий решений для агента
28 июл 2026, 17:22
28 июл 2026, 17:22
b4de763
Код
Авторство
О чём код?
import { useMemo, useState } from 'react'; import { Alert, Box, Button, Card, CardContent, Chip, Collapse, Link, Stack, Tooltip, Typography, } from '@mui/material'; import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import LaunchIcon from '@mui/icons-material/Launch'; import { PRIORITY_CHIP, clusterColor } from '../theme'; function TaskCard({ task, clusters, taskSignals, onApprove, onReject, onOpenSource }) { const [busy, setBusy] = useState(null); // 'approve' | 'reject' | null const [error, setError] = useState(null); const [signalsOpen, setSignalsOpen] = useState(false); const cluster = clusters.find((c) => c.id === task.clusterId); const run = async (kind, action) => { setBusy(kind); setError(null); try { await action(task.id); } catch (e) { setError(e.message); } finally { setBusy(null); } }; const rejected = task.status === 'rejected'; return ( <Card variant="outlined" sx={{ opacity: rejected ? 0.6 : 1 }}> <CardContent> <Stack direction="row" spacing={1} alignItems="flex-start" justifyContent="space-between"> <Typography variant="subtitle1" fontWeight={600}> {task.title} </Typography> <Tooltip title="Приоритет из score потребности: частота × эмоция × охват сегментов. Точная формула — во вкладке «Журнал анализа»."> <Chip size="small" color={PRIORITY_CHIP[task.priority] || 'default'} label={task.priority} /> </Tooltip> </Stack> <Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 0.5, mb: 1 }}> {cluster && ( <Tooltip title="Потребность, из которой родилась задача. Кликни на кластер выше, чтобы увидеть её сигналы на карте."> <Chip size="small" variant="outlined" icon={ <Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: clusterColor(clusters, cluster.id), ml: '6px !important', }} /> } label={cluster.name} /> </Tooltip> )} <Tooltip title="Раскрыть сигналы-доказательства: цитаты и первичные документы, из которых родилась задача."> <Button size="small" color="inherit" sx={{ textTransform: 'none', color: 'text.secondary', py: 0 }} endIcon={signalsOpen ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} onClick={() => setSignalsOpen(!signalsOpen)} > Сигналов: {taskSignals.length} </Button> </Tooltip> </Stack> <Collapse in={signalsOpen} unmountOnExit> <Box sx={{ mb: 1.5, pl: 1, borderLeft: 2, borderColor: 'divider', maxHeight: 260, overflowY: 'auto', }} > {taskSignals.map((s) => ( <Box key={s.id} sx={{ mb: 1 }}> <Tooltip title="Открыть первичный документ (полный текст интервью или обращения)"> <Chip size="small" variant="outlined" clickable onClick={() => onOpenSource(s.sourceId)} label={`${s.source === 'интервью' ? '🎙' : '🎫'} ${s.sourceRef}`} /> </Tooltip> <Typography variant="body2" color="text.secondary" sx={{ mt: 0.3 }}> «{s.text}» </Typography> </Box> ))} </Box> </Collapse> <Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}> {task.description} </Typography> {task.similarTask && ( <Alert severity={task.status === 'pending' ? 'warning' : 'info'} sx={{ mb: 1.5 }}> Пересекается с задачей бэклога:{' '} <Tooltip title="Открыть исходную задачу бэклога целиком"> <Link component="button" type="button" onClick={() => onOpenSource(task.similarTask.sourceId)} sx={{ fontWeight: 600, verticalAlign: 'baseline' }} > {task.similarTask.jiraId} </Link> </Tooltip>{' '} — {task.similarTask.title} </Alert> )} {error && ( <Alert severity="error" sx={{ mb: 1.5 }} onClose={() => setError(null)}> {error} </Alert> )} {task.status === 'pending' && ( <Stack direction="row" spacing={1}> <Tooltip title="Задача будет помечена принятой — агент заведёт её в СберТрек через MCP с готовым описанием."> <span> <Button variant="contained" size="small" startIcon={<CheckIcon />} disabled={busy !== null} onClick={() => run('approve', onApprove)} > {busy === 'approve' ? 'Заводим…' : 'Завести в СберТрек'} </Button> </span> </Tooltip> <Tooltip title="Задача останется в журнале зачёркнутой — решение можно объяснить на защите."> <span> <Button variant="outlined" color="inherit" size="small" startIcon={<CloseIcon />} disabled={busy !== null} onClick={() => run('reject', onReject)} > {busy === 'reject' ? 'Отклоняем…' : 'Отклонить'} </Button> </span> </Tooltip> </Stack> )} {task.status === 'approved' && ( <Chip color="success" size="small" icon={<CheckIcon />} label={ <span> Заведена в СберТрек:{' '} {task.jiraUrl ? ( <Link href={task.jiraUrl} target="_blank" rel="noreferrer" color="inherit"> {task.jiraId} <LaunchIcon sx={{ fontSize: 12, verticalAlign: 'middle' }} /> </Link> ) : ( task.jiraId )} </span> } /> )} {rejected && <Chip size="small" icon={<CloseIcon />} label="Отклонена" />} </CardContent> </Card> ); } export default function TasksPanel({ tasks, clusters, signals, onApprove, onReject, onOpenSource }) { const pending = tasks.filter((t) => t.status === 'pending').length; const signalById = useMemo( () => Object.fromEntries((signals || []).map((s) => [s.id, s])), [signals] ); return ( <Box> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> Сервер предложил {tasks.length} задач(и) на основе кластеров сигналов {pending > 0 ? `, ожидают решения: ${pending}` : ' — все обработаны'}. </Typography> <Box sx={{ display: 'grid', gap: 2, gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, alignItems: 'start', }} > {tasks.map((task) => ( <TaskCard key={task.id} task={task} clusters={clusters} taskSignals={task.signalIds.map((id) => signalById[id]).filter(Boolean)} onApprove={onApprove} onReject={onReject} onOpenSource={onOpenSource} /> ))} </Box> </Box> ); }