/
sweetebin
/
hackaton_26
Обзор
Документация
Войти
/
sweetebin
/
hackaton_26
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
web-form-project/src/components/ProjectView.jsx
224 строки
7 KB
sweetebin
Трассировка до первоисточника в UI + CLI-инструментарий решений для агента
28 июл 2026, 17:22
28 июл 2026, 17:22
b4de763
Код
Авторство
О чём код?
import { useMemo, useState } from 'react'; import { Alert, Box, Button, Chip, CircularProgress, IconButton, Stack, Tab, Tabs, Tooltip, Typography, } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import AddIcon from '@mui/icons-material/Add'; import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import NotesIcon from '@mui/icons-material/Notes'; import { useProject } from '../hooks/useProject'; import AnalysisProgress from './AnalysisProgress'; import AddDataDialog from './AddDataDialog'; import ClustersView from './ClustersView'; import SemanticMap from './SemanticMap'; import SignalDetails from './SignalDetails'; import SourceDialog from './SourceDialog'; import SourcesSetup from './SourcesSetup'; import TasksPanel from './TasksPanel'; import JournalView from './JournalView'; const POOL_LABELS = { interview: 'Интервью', ticket: 'Обращения', backlog: 'Бэклог', context: 'Контекст', }; function SectionTitle({ children }) { return ( <Typography variant="h6" sx={{ mt: 4, mb: 2 }}> {children} </Typography> ); } export default function ProjectView({ projectId, onBack }) { const { project, phase, error, addData, startAnalysis, approve, reject, reload } = useProject(projectId); const [selectedClusterId, setSelectedClusterId] = useState(null); const [selectedSignalId, setSelectedSignalId] = useState(null); const [openSourceId, setOpenSourceId] = useState(null); const [addOpen, setAddOpen] = useState(false); const [tab, setTab] = useState(0); const result = phase === 'done' ? project?.result : null; const selectedSignal = useMemo( () => result?.signals.find((s) => s.id === selectedSignalId) || null, [result, selectedSignalId] ); const handleAddData = async (payload) => { setSelectedClusterId(null); setSelectedSignalId(null); await addData(payload); }; return ( <Box> <Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 1 }}> <IconButton onClick={onBack} aria-label="К списку проектов"> <ArrowBackIcon /> </IconButton> <Typography variant="h6" sx={{ flexGrow: 1 }}> {project ? project.name : 'Загрузка проекта…'} </Typography> {project && ( <Tooltip title="Пространство СберТрек, в которое заводятся принятые задачи"> <Chip variant="outlined" label={project.jiraProjectKey} /> </Tooltip> )} {phase !== 'setup' && ( <Tooltip title={ phase === 'done' ? 'Вставить текст интервью/обращения или загрузить файл — новые данные добавятся к пулу, и анализ пересчитается.' : 'Дождитесь окончания анализа — добавить данные можно будет после.' } > <span> <Button variant="contained" startIcon={<AddIcon />} disabled={phase !== 'done'} onClick={() => setAddOpen(true)} > Добавить данные </Button> </span> </Tooltip> )} </Stack> {phase !== 'setup' && project && project.sources.length > 0 && ( <Stack direction="row" spacing={1} useFlexGap flexWrap="wrap" sx={{ mb: 2, ml: 6.5 }}> <Typography variant="body2" color="text.secondary" sx={{ mr: 0.5, alignSelf: 'center' }}> Пул данных: </Typography> {Object.entries( project.sources.reduce((acc, s) => { (acc[s.kind] = acc[s.kind] || []).push(s); return acc; }, {}) ).map(([kind, items]) => ( <Tooltip key={kind} title={items .slice(0, 30) .map((s) => s.name) .join(', ') + (items.length > 30 ? '…' : '')} > <Chip size="small" variant="outlined" icon={kind === 'interview' ? <NotesIcon /> : <InsertDriveFileIcon />} label={`${POOL_LABELS[kind] || kind}: ${items.length}`} /> </Tooltip> ))} </Stack> )} <Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 1, borderBottom: 1, borderColor: 'divider' }}> <Tab label={ <Tooltip title="Кластеры потребностей, семантическая карта сигналов и предложенные задачи."> <span>Обзор</span> </Tooltip> } /> <Tab label={ <Tooltip title="Прозрачность конвейера: каждый извлечённый сигнал, склейки-дубликаты, аудит бэклога и судьба каждой задачи."> <span>Журнал анализа</span> </Tooltip> } /> </Tabs> {phase === 'loading' && ( <Box sx={{ textAlign: 'center', py: 8 }}> <CircularProgress /> </Box> )} {phase === 'setup' && tab === 0 && project && ( <SourcesSetup project={project} onReload={reload} onStart={startAnalysis} /> )} {phase === 'processing' && tab === 0 && <AnalysisProgress />} {phase === 'error' && ( <Alert severity="error" sx={{ maxWidth: 720 }}> {error} </Alert> )} {tab === 1 && phase !== 'loading' && ( <JournalView projectId={projectId} phase={phase} /> )} {tab === 0 && result && ( <> <SectionTitle>Кластеры сигналов</SectionTitle> <ClustersView clusters={result.clusters} selectedClusterId={selectedClusterId} onSelectCluster={setSelectedClusterId} /> <SectionTitle>Семантическая карта</SectionTitle> <Box sx={{ display: 'grid', gap: 2, gridTemplateColumns: { xs: '1fr', md: '2fr 1fr' }, alignItems: 'stretch', }} > <SemanticMap signals={result.signals} clusters={result.clusters} selectedClusterId={selectedClusterId} onSelectCluster={setSelectedClusterId} selectedSignalId={selectedSignalId} onSelectSignal={setSelectedSignalId} /> <SignalDetails signal={selectedSignal} clusters={result.clusters} onOpenSource={setOpenSourceId} /> </Box> <SectionTitle>Предложенные задачи</SectionTitle> <TasksPanel tasks={result.suggestedTasks} clusters={result.clusters} signals={result.signals} onApprove={approve} onReject={reject} onOpenSource={setOpenSourceId} /> </> )} <SourceDialog projectId={projectId} sourceId={openSourceId} onClose={() => setOpenSourceId(null)} /> <AddDataDialog open={addOpen} onClose={() => setAddOpen(false)} onSubmit={handleAddData} /> </Box> ); }