From b7e04308ec931cf4796a33eb2bd34c929bc6f8b7 Mon Sep 17 00:00:00 2001 From: Link <131011403+LinkF2kkk@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:02:32 +0300 Subject: [PATCH 1/2] Add report modal and wire export flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a ReportModal component to preview analysis results, highlight issues in text, filter by category and choose export format. App now tracks editorText and showReport; analyze stores the current text, handleExport uses the stored text (language set to 'ru'), and reset is passed directly as onClear. Editor and Toolbar props updated to support onOpenReport, and Toolbar shows a "Посмотреть отчёт" button to open the modal. Also includes minor style/formatting tweaks across components. --- client/src/App.js | 31 ++- client/src/components/Editor.jsx | 4 +- client/src/components/ReportModal.jsx | 358 ++++++++++++++++++++++++++ client/src/components/Toolbar.jsx | 144 +++++------ 4 files changed, 452 insertions(+), 85 deletions(-) create mode 100644 client/src/components/ReportModal.jsx diff --git a/client/src/App.js b/client/src/App.js index 65037a7..cedae0a 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,20 +1,21 @@ -import Editor from './components/Editor' -import Results from './components/Results' +import { useState } from 'react' +import Editor from './components/Editor' +import Results from './components/Results' +import ReportModal from './components/ReportModal' import { useAnalysis } from './hooks/useAnalysis' export default function App() { const { result, loading, error, analyze, exportReport, reset } = useAnalysis() + const [showReport, setShowReport] = useState(false) + const [editorText, setEditorText] = useState('') const handleAnalyze = async (text, language) => { + setEditorText(text) return await analyze(text, language) } - const handleExport = (format, text, language) => { - exportReport(format, text, result, language) - } - - const handleClear = () => { - reset() + const handleExport = (format) => { + exportReport(format, editorText, result, 'ru') } return ( @@ -28,8 +29,8 @@ export default function App() { issues={result?.issues} result={result} onAnalyze={handleAnalyze} - onExport={handleExport} - onClear={handleClear} + onOpenReport={() => setShowReport(true)} + onClear={reset} /> {error && ( @@ -45,6 +46,16 @@ export default function App() { )} + + {showReport && ( + setShowReport(false)} + onExport={handleExport} + /> + )} ) } \ No newline at end of file diff --git a/client/src/components/Editor.jsx b/client/src/components/Editor.jsx index 4091a8a..27e5dd3 100644 --- a/client/src/components/Editor.jsx +++ b/client/src/components/Editor.jsx @@ -9,7 +9,7 @@ import { DecorationSet } from 'prosemirror-view' import IssuePopup from './IssuePopup' import Toolbar from './Toolbar' -export default function Editor({ onAnalyze, onExport, loading, issues, result }) { +export default function Editor({ onAnalyze, onExport, loading, issues, result, onOpenReport }) { const [detectedLang, setDetectedLang] = useState('ru') const [langTimer, setLangTimer] = useState(null) const [popup, setPopup] = useState(null) @@ -108,7 +108,7 @@ export default function Editor({ onAnalyze, onExport, loading, issues, result }) hasResult={!!result} onAnalyze={handleAnalyze} onClear={handleClear} - onExport={handleExport} + onOpenReport={onOpenReport} /> {/* Легенда */} diff --git a/client/src/components/ReportModal.jsx b/client/src/components/ReportModal.jsx new file mode 100644 index 0000000..4da4b16 --- /dev/null +++ b/client/src/components/ReportModal.jsx @@ -0,0 +1,358 @@ +import { useState } from 'react' +import { CATEGORY_COLORS } from '../constants/categories' + +const CATEGORY_ICONS = { + 'Орфография': '🔤', + 'Пунктуация': '✍️', + 'Грамматика': '📖', + 'Тавтология': '🔁', + 'Стиль': '🎨', + 'Канцеляризм': '📋', +} + +function HighlightedText({ text, issues }) { + if (!text || !issues?.length) return

{text}

+ + // Собираем все вхождения с их индексами + const marks = [] + issues.forEach((issue, issueIdx) => { + if (!issue.original) return + let start = 0 + while (true) { + const idx = text.indexOf(issue.original, start) + if (idx === -1) break + marks.push({ + from: idx, + to: idx + issue.original.length, + color: CATEGORY_COLORS[issue.category] || '#E2E8F0', + issueIdx + }) + start = idx + 1 + } + }) + + // Сортируем по позиции + marks.sort((a, b) => a.from - b.from) + + // Строим части текста + const parts = [] + let cursor = 0 + + marks.forEach(({ from, to, color, issueIdx }) => { + if (from < cursor) return // перекрытие — пропускаем + if (cursor < from) { + parts.push( + {text.slice(cursor, from)} + ) + } + parts.push( + + {text.slice(from, to)} + + ) + cursor = to + }) + + if (cursor < text.length) { + parts.push({text.slice(cursor)}) + } + + return ( +

+ {parts} +

+ ) +} + +export default function ReportModal({ result, text, language, onClose, onExport }) { + const [selectedFormat, setSelectedFormat] = useState('docx') + const [activeCategory, setActiveCategory] = useState(null) + + if (!result) return null + + const issues = result.issues || [] + const stats = result.statistics || {} + const grouped = issues.reduce((acc, issue) => { + if (!acc[issue.category]) acc[issue.category] = [] + acc[issue.category].push(issue) + return acc + }, {}) + + const categories = Object.keys(grouped) + const filteredIssues = activeCategory + ? grouped[activeCategory] || [] + : issues + + return ( + // Затемнённый фон +
e.target === e.currentTarget && onClose()} + style={{ + position: 'fixed', + inset: 0, + backgroundColor: 'rgba(0,0,0,0.6)', + zIndex: 1000, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '24px' + }} + > + {/* Основное окно */} +
+ + {/* Шапка */} +
+
+

+ Отчёт анализа текста +

+ + Найдено проблем: {issues.length} · Слов: {stats.wordCount} · Предложений: {stats.sentenceCount} + +
+ +
+ + {/* Три колонки */} +
+ + {/* ===== Левая панель — список ошибок ===== */} +
+ {/* Фильтр по категории */} +
+ + {categories.map(cat => ( + + ))} +
+ + {/* Список ошибок */} +
+ {filteredIssues.map((issue, i) => ( +
+
+ + {issue.category} + +
+

+ {issue.description} +

+
+
+ ✗ {issue.original} +
+
+ ✓ {issue.suggestion} +
+
+
+ ))} +
+
+ + {/* ===== Центральная часть — текст с подсветкой ===== */} +
+

+ ПРЕДПРОСМОТР ТЕКСТА +

+ +
+ + {/* ===== Правая панель — экспорт ===== */} +
+
+

+ ЭКСПОРТ +

+ + {/* Выбор формата */} +
+ {[ + { format: 'docx', label: 'Word (.docx)', icon: '📄', color: '#2563eb' }, + { format: 'pdf', label: 'PDF (.pdf)', icon: '📕', color: '#dc2626' }, + ].map(({ format, label, icon, color }) => ( + + ))} +
+ + {/* Кнопка скачать */} + +
+ + {/* Место для будущего функционала */} +
+ Здесь будет дополнительный функционал +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/client/src/components/Toolbar.jsx b/client/src/components/Toolbar.jsx index 6afd7f8..401e837 100644 --- a/client/src/components/Toolbar.jsx +++ b/client/src/components/Toolbar.jsx @@ -1,24 +1,24 @@ import { useState } from 'react' -export default function Toolbar({ loading, hasResult, onAnalyze, onClear, onExport }) { +export default function Toolbar({ loading, hasResult, onAnalyze, onClear, onOpenReport, onExport }) { const [showExportMenu, setShowExportMenu] = useState(false) return (
- + {/* Анализировать */} - {/* Скачать — только если есть результат */} + {/* Кнопка отчета — только если есть результат */} {hasResult && ( -
- + + )} - {/* Выпадающее меню */} - {showExportMenu && ( -
- {[ - { format: 'docx', label: '📄 Word (.docx)', color: '#2563eb' }, - { format: 'pdf', label: '📕 PDF (.pdf)', color: '#dc2626' }, - ].map(({ format, label, color }) => ( - - ))} -
- )} + {/* Выпадающее меню экспорта */} + {showExportMenu && ( +
+ {[ + { format: 'docx', label: '📄 Word (.docx)', color: '#2563eb' }, + { format: 'pdf', label: '📕 PDF (.pdf)', color: '#dc2626' }, + ].map(({ format, label, color }) => ( + + ))}
)}
From 84c677be9ad25648f0745f7703ad4c6fd1f721e9 Mon Sep 17 00:00:00 2001 From: Link <131011403+LinkF2kkk@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:06:51 +0300 Subject: [PATCH 2/2] Clear ane editing files --- Controllers/WeatherForecastController.cs | 33 ------------------------ Program.cs | 13 +++------- Services/OpenAiService.cs | 4 +-- WeatherForecast.cs | 13 ---------- 4 files changed, 5 insertions(+), 58 deletions(-) delete mode 100644 Controllers/WeatherForecastController.cs delete mode 100644 WeatherForecast.cs diff --git a/Controllers/WeatherForecastController.cs b/Controllers/WeatherForecastController.cs deleted file mode 100644 index e1a0ae3..0000000 --- a/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace WebEditor.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/Program.cs b/Program.cs index 25539c1..fd003d7 100644 --- a/Program.cs +++ b/Program.cs @@ -11,12 +11,12 @@ public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); - // База данных + // DATABASE builder.Services.AddDbContext(options => options.UseSqlServer( builder.Configuration.GetConnectionString("Default"))); - // Сервисы + // SEVICES builder.Services.AddSingleton(); // CORS @@ -36,14 +36,7 @@ public static void Main(string[] args) app.UseHttpsRedirection(); app.UseCors(); - // Тест БД - app.MapGet("/test-db", async (ServerWebDataBaseContext db) => - { - var ok = await db.Database.CanConnectAsync(); - return Results.Ok(ok ? "БД подключена!" : "Ошибка подключения"); - }); - - // Эндпоинты + // ENDPOINTS app.MapAnalysisEndpoints(); app.Run(); diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs index 685ef8f..468e108 100644 --- a/Services/OpenAiService.cs +++ b/Services/OpenAiService.cs @@ -76,7 +76,7 @@ 8. Посчитать статистику текста var requestBody = new { model = _model, - temperature = 0.1, // низкая температура = точнее и стабильнее + temperature = 0.1, // TEMPERATURE MODELS (0.1 = focused and deterministic, 0.9 = creative and random) messages = new[] { new { role = "system", content = systemPrompt }, @@ -103,7 +103,7 @@ 8. Посчитать статистику текста .GetProperty("content") .GetString(); - // Очищаем на случай если модель всё же добавила markdown + // DELETE MARKDOWN resultText = resultText? .Replace("```json", "") .Replace("```", "") diff --git a/WeatherForecast.cs b/WeatherForecast.cs deleted file mode 100644 index 79eed00..0000000 --- a/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace WebEditor -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -}