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 @@ public async Task AnalyzeTextAsync(string text, string language = "ru") 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 @@ public async Task AnalyzeTextAsync(string text, string language = "ru") .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; } - } -} 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 }) => ( + + ))}
)}