import { useState, useRef, useEffect } from 'react'; import { open } from '@tauri-apps/plugin-dialog'; import { listen } from '@tauri-apps/api/event'; import { MessageSquare, RotateCcw, Trash2, FolderOpen, FolderPlus, File, Download, FileDown, User, Bot, Info, RefreshCw, GitCompare, History, X, } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import { analyzeProject, type AnalyzeReport, type Action, type ApplyResult, type UndoResult, type PreviewResult, type DiffItem } from '../lib/analyze'; import { animateFadeInUp } from '../lib/anime-utils'; type Message = | { role: 'user'; text: string } | { role: 'system'; text: string } | { role: 'assistant'; text: string } | { role: 'assistant'; report: AnalyzeReport; error?: string }; type HistoryItem = { path: string; ts: number; projectType?: string; risk?: string; issueCount?: number; summary?: string; report: AnalyzeReport; }; const UNDO_SYSTEM_MESSAGE = 'Последнее действие отменено.'; const HISTORY_MAX = 20; export function Tasks() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isAnalyzing, setIsAnalyzing] = useState(false); const [lastReport, setLastReport] = useState(null); const [lastPath, setLastPath] = useState(null); const [previousReport, setPreviousReport] = useState(null); const [history, setHistory] = useState([]); const [historyOpen, setHistoryOpen] = useState(false); const [selectedActions, setSelectedActions] = useState>({}); const [undoAvailable, setUndoAvailable] = useState(false); const [pendingPreview, setPendingPreview] = useState<{ path: string; actions: Action[]; diffs: DiffItem[]; } | null>(null); const [isPreviewing, setIsPreviewing] = useState(false); const messagesEndRef = useRef(null); const containerRef = useRef(null); const messagesListRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); useEffect(() => { if (messages.length === 0) return; const t = setTimeout(() => { const last = messagesListRef.current?.querySelector('.message-item-anime:last-child'); if (last) animateFadeInUp(last, { duration: 500 }); }, 50); return () => clearTimeout(t); }, [messages.length]); useEffect(() => { const unlisten = listen('analyze_progress', (e) => { if (e.payload) { setMessages((prev) => [...prev, { role: 'system', text: e.payload }]); } }); return () => { unlisten.then((fn) => fn()); }; }, []); const handleClearChat = () => { setMessages([]); }; const handleUndo = () => { if (messages.length === 0) return; setMessages((prev) => { const next = [...prev]; while (next.length > 0) { const last = next[next.length - 1]; next.pop(); if (last.role === 'user') break; } next.push({ role: 'system', text: UNDO_SYSTEM_MESSAGE }); return next; }); }; const handleSend = () => { if (!input.trim()) return; setMessages((prev) => [...prev, { role: 'user', text: input.trim() }]); setInput(''); setTimeout(() => { setMessages((prev) => [ ...prev, { role: 'assistant', text: 'Ответ ИИ агента будет отображаться здесь. Результаты действий агента подключаются к backend.' }, ]); }, 500); }; const runAnalysis = async (pathStr: string) => { setIsAnalyzing(true); setMessages((prev) => [ ...prev, { role: 'user', text: `Проанализируй проект: ${pathStr}` }, { role: 'assistant', text: 'Индексирую файлы…' }, ]); try { const report = await analyzeProject(pathStr); setPreviousReport(lastReport); setLastReport(report); setLastPath(pathStr); const init: Record = {}; (report.actions ?? []).forEach((a) => { init[a.id] = true; }); setSelectedActions(init); setUndoAvailable(false); setPendingPreview(null); setHistory((prev) => { const item: HistoryItem = { path: report.path ?? pathStr, ts: Date.now(), projectType: report.structure?.project_type, risk: report.project_context?.risk_level, issueCount: report.findings?.length ?? 0, summary: report.narrative?.slice(0, 80) + (report.narrative?.length > 80 ? '…' : ''), report, }; const next = [item, ...prev].slice(0, HISTORY_MAX); return next; }); setMessages((prev) => { const next = [...prev]; for (let i = next.length - 1; i >= 0; i--) { if (next[i].role === 'assistant' && 'text' in next[i]) { next[i] = { role: 'assistant', report }; break; } } return next; }); } catch (e) { const errMsg = e instanceof Error ? e.message : String(e); setMessages((prev) => { const next = [...prev]; for (let i = next.length - 1; i >= 0; i--) { if (next[i].role === 'assistant' && 'text' in next[i]) { next[i] = { role: 'assistant', report: {} as AnalyzeReport, error: errMsg }; break; } } return next; }); } finally { setIsAnalyzing(false); } }; const handlePickFolderAndAnalyze = async () => { const selected = await open({ directory: true, multiple: false }); if (!selected) return; await runAnalysis(selected); }; const handlePickFileAndAnalyze = async () => { const selected = await open({ directory: false, multiple: false }); if (!selected) return; const pathStr = typeof selected === 'string' ? selected : selected[0] ?? ''; if (!pathStr) return; const parentDir = pathStr.replace(/[/\\][^/\\]+$/, '') || pathStr; await runAnalysis(parentDir); }; const handlePickFoldersAndAnalyze = async () => { const selected = await open({ directory: true, multiple: true }); if (!selected) return; const paths = Array.isArray(selected) ? selected : [selected]; if (paths.length === 0) return; if (paths.length > 1) { setMessages((prev) => [...prev, { role: 'system', text: `Выбрано папок: ${paths.length}. Анализирую первую.` }]); } await runAnalysis(paths[0]); }; const handleRepeatAnalysis = () => { if (lastPath) runAnalysis(lastPath); }; const handleCompareWithPrevious = () => { if (!lastReport || !previousReport) return; const curr = lastReport.stats; const prev = previousReport.stats; const diffFiles = curr.file_count - prev.file_count; const diffDirs = curr.dir_count - prev.dir_count; const text = diffFiles === 0 && diffDirs === 0 ? 'Предыдущий и текущий отчёт совпадают по числу файлов и папок.' : `Сравнение с предыдущим анализом:\n\nФайлов: ${prev.file_count} → ${curr.file_count} (${diffFiles >= 0 ? '+' : ''}${diffFiles})\nПапок: ${prev.dir_count} → ${curr.dir_count} (${diffDirs >= 0 ? '+' : ''}${diffDirs})\n\nТип тогда: ${previousReport.structure?.project_type ?? '—'}\nТип сейчас: ${lastReport.structure?.project_type ?? '—'}`; setMessages((p) => [...p, { role: 'assistant', text }]); }; const handleCompareWithHistory = (item: HistoryItem) => { if (!lastReport) return; const curr = lastReport.stats; const prev = item.report.stats; const diffFiles = curr.file_count - prev.file_count; const diffDirs = curr.dir_count - prev.dir_count; const text = `Сравнение с историей (${new Date(item.ts).toLocaleString('ru-RU')}):\n\nФайлов: ${prev.file_count} → ${curr.file_count} (${diffFiles >= 0 ? '+' : ''}${diffFiles})\nПапок: ${prev.dir_count} → ${curr.dir_count} (${diffDirs >= 0 ? '+' : ''}${diffDirs})\nПроблем: ${item.issueCount ?? 0} → ${lastReport.findings?.length ?? 0}\n\nТип тогда: ${item.projectType ?? '—'}\nТип сейчас: ${lastReport.structure?.project_type ?? '—'}\nРиск тогда: ${item.risk ?? '—'}\nРиск сейчас: ${lastReport.project_context?.risk_level ?? '—'}`; setMessages((p) => [...p, { role: 'assistant', text }]); setHistoryOpen(false); }; const handleDownloadReport = (report: AnalyzeReport) => { const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'papa-yu-report.json'; a.click(); URL.revokeObjectURL(url); }; const handleDownloadMD = (report: AnalyzeReport) => { const md = report.report_md ?? report.narrative ?? ''; const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'papa-yu-report.md'; a.click(); URL.revokeObjectURL(url); }; const pushSystem = (text: string) => { setMessages((p) => [...p, { role: 'system', text }]); }; const pushAssistant = (text: string) => { setMessages((p) => [...p, { role: 'assistant', text }]); }; function clip(s: string, n = 1200) { if (!s) return ''; return s.length > n ? s.slice(0, n) + '\n…(обрезано)…' : s; } function renderPreviewText(diffs: DiffItem[]) { const lines: string[] = []; lines.push('Вот что изменится:\n\n'); diffs.forEach((d, i) => { lines.push(`${i + 1}. ${d.summary}`); if (d.kind === 'create' || d.kind === 'update') { if (d.before != null) { lines.push(`— До:\n\`\`\`\n${clip(d.before)}\n\`\`\``); } if (d.after != null) { lines.push(`— После:\n\`\`\`\n${clip(d.after)}\n\`\`\``); } } if (d.kind === 'delete' && d.before != null) { lines.push(`— Будет удалено содержимое:\n\`\`\`\n${clip(d.before)}\n\`\`\``); } lines.push(''); }); lines.push('Если всё выглядит правильно — нажмите «Применить». Иначе — «Отмена».'); return lines.join('\n'); } const handlePreview = async (projectPath: string, actions: Action[]) => { const selected = actions.filter((a) => selectedActions[a.id]); if (!selected.length) return; setIsPreviewing(true); try { const res = await invoke('preview_actions', { payload: { path: projectPath, actions: selected }, }); setIsPreviewing(false); if (!res.ok) { pushSystem('Не удалось сформировать предпросмотр изменений.'); return; } setPendingPreview({ path: projectPath, actions: selected, diffs: res.diffs }); pushSystem('Подготовил предпросмотр изменений.'); pushAssistant(renderPreviewText(res.diffs)); } catch (e) { setIsPreviewing(false); pushSystem(String(e ?? 'Ошибка предпросмотра.')); } }; const handleApplyPending = async () => { if (!pendingPreview) return; const { path, actions } = pendingPreview; try { const res = await invoke('apply_actions', { payload: { path, actions }, }); if (res.ok) { pushSystem('Изменения применены.'); setUndoAvailable(true); } else { pushSystem(res.error ?? 'Изменения не применены. Откат выполнен.'); setUndoAvailable(false); } } catch (e) { pushSystem(String(e ?? 'Ошибка применения.')); setUndoAvailable(false); } setPendingPreview(null); }; const handleCancelPending = () => { if (!pendingPreview) return; setPendingPreview(null); pushSystem('Предпросмотр отменён. Ничего не изменено.'); }; const handleUndoLast = async (projectPath: string) => { try { const res = await invoke('undo_last', { path: projectPath }); if (res.ok) { pushSystem('Откат выполнен.'); setUndoAvailable(false); } else { pushSystem(res.error ?? 'Откат недоступен.'); } } catch (e) { pushSystem(String(e ?? 'Ошибка отката.')); } }; // handleApplyActions removed: Apply goes through Preview → handleApplyPending return (
PAPA YU
{lastPath && ( )} {lastReport && previousReport && ( )}

Анализ проекта

{messages.length === 0 ? (

Выберите папку проекта для анализа.

Или введите путь или сообщение ниже.

) : (
{messages.map((m, i) => (
{m.role !== 'system' && (
{m.role === 'user' ? ( ) : ( )}
)} {m.role === 'system' && (
)}
{m.role === 'system' &&
{m.text}
} {m.role === 'user' &&
{m.text}
} {m.role === 'assistant' && 'text' in m && (
{m.text}
)} {m.role === 'assistant' && 'report' in m && m.report && ( )}
))}
)}
{historyOpen && (
setHistoryOpen(false)}>
e.stopPropagation()}>

История анализов

{history.length === 0 ? (

Пока нет записей. Запустите анализ папки.

) : ( history.map((item, i) => (

{item.path}

{item.projectType ?? '—'} · риск {item.risk ?? '—'} · проблем {item.issueCount ?? 0}

)) )}
)} {pendingPreview && ( )}
setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSend()} placeholder="Сообщение или путь к папке..." className="flex-1 px-4 py-2.5 border rounded-xl bg-background focus:outline-none focus:ring-2 focus:ring-primary/50" />
); } function PreviewDialog({ diffs, onApply, onCancel, }: { diffs: DiffItem[]; onApply: () => void; onCancel: () => void; }) { const [expanded, setExpanded] = useState>({}); const [tab, setTab] = useState<'preview' | 'verify' | 'write'>('preview'); const toggle = (i: number) => setExpanded((p) => ({ ...p, [i]: !p[i] })); return (

Предпросмотр изменений

{(['preview', 'verify', 'write'] as const).map((t) => ( ))}
{tab === 'preview' && (
    {diffs.map((d, i) => (
  • {expanded[i] && (
    {d.before != null && (

    До:

    {d.before}
    )} {d.after != null && (

    После:

    {d.after}
    )} {d.kind === 'delete' && d.before == null && d.after == null && (

    Файл или каталог будет удалён.

    )}
    )}
  • ))}
)} {tab === 'verify' && (

Проверка типов и сборки после применения будет доступна в следующей версии.

)} {tab === 'write' && (

Написание и генерация кода по результатам проверки — в разработке.

)}
); } function PriorityBadge({ priority }: { priority: string }) { const p = (priority || '').toLowerCase(); const style = p === 'high' ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' : p === 'medium' ? 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400' : 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400'; const label = p === 'high' ? 'high' : p === 'medium' ? 'medium' : 'low'; return {label}; } function ReportBlock({ report, error, onDownload, onDownloadMD, isCurrentReport, selectedActions, setSelectedActions, undoAvailable, hasPendingPreview, isPreviewing, onPreview, onApplyPending, onCancelPending, onUndo, }: { report: AnalyzeReport; error?: string; onDownload: (r: AnalyzeReport) => void; onDownloadMD: (r: AnalyzeReport) => void; isCurrentReport: boolean; selectedActions: Record; setSelectedActions: React.Dispatch>>; undoAvailable: boolean; hasPendingPreview: boolean; isPreviewing: boolean; onPreview: (projectPath: string, actions: Action[]) => void; onApplyPending: () => void; onCancelPending: () => void; onUndo: (projectPath: string) => void; }) { if (error) { return
Ошибка: {error}
; } const r = report as AnalyzeReport; const hasReport = r && (r.path || r.narrative || r.findings?.length || r.recommendations?.length); const ctx = r.project_context; const recs = r.recommendations ?? []; const actions = r.actions ?? []; return (
{hasReport && ( <> {r.narrative && (
{r.narrative}
)} {ctx && (ctx.stack?.length || ctx.maturity || ctx.risk_level) && (

Контекст проекта

{[ctx.stack?.join(', '), ctx.maturity, ctx.risk_level && `риск ${ctx.risk_level}`].filter(Boolean).join(' · ')}

)} {r.structure && (r.structure.project_type || r.structure.architecture) && (
{r.structure.project_type && (

Тип проекта:{' '} {r.structure.project_type}

)} {r.structure.architecture && (

Архитектура:{' '} {r.structure.architecture}

)}
)} {r.findings?.length > 0 && (

Находки

    {r.findings.slice(0, 10).map((f, i) => (
  • {f.title} {f.details && ` — ${f.details}`}
  • ))}
)} {recs.length > 0 && (

Топ-рекомендации

    {recs.slice(0, 5).map((rec, i) => (
  • {rec.title} {(rec.effort || rec.impact) && ( (effort: {rec.effort ?? '—'}, impact: {rec.impact ?? '—'}) )}
  • ))}
)} {isCurrentReport && actions.length > 0 && (

Исправления

    {actions.map((a) => (
  • setSelectedActions((prev) => ({ ...prev, [a.id]: !prev[a.id] }))} className="rounded border-border" />
  • ))}
{!hasPendingPreview ? ( ) : ( <> )} {undoAvailable && ( )}
)}
{(r.report_md ?? r.narrative) && ( )}
)}
); }