Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Controllers/TextRequest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace WebEditor.Controllers.Requests
{
public record TextRequest(string Text, string? Language = "ru");
public record TextRequest(
string Text,
string? Language = "ru",
string? UserPrompt = null);
}
6 changes: 5 additions & 1 deletion Endpoints/AnalysisEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ public static void MapAnalysisEndpoints(this WebApplication app)
if (string.IsNullOrWhiteSpace(request.Text))
return Results.BadRequest("Текст не может быть пустым");

var result = await openAi.AnalyzeTextAsync(request.Text, request.Language ?? "ru");
var result = await openAi.AnalyzeTextAsync(
request.Text,
request.Language ?? "ru",
request.UserPrompt // ← новое поле
);
return Results.Ok(result);
});
}
Expand Down
101 changes: 52 additions & 49 deletions Services/OpenAiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class OpenAiService
private readonly HttpClient _httpClient;
private readonly string _model;
private readonly ILogger<OpenAiService> _logger;
private readonly string _systemPromptTemplate;

public OpenAiService(IConfiguration config, ILogger<OpenAiService> logger)
{
Expand All @@ -20,79 +21,73 @@ public OpenAiService(IConfiguration config, ILogger<OpenAiService> logger)
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", config["OpenAI:ApiKey"]);
_httpClient.Timeout = TimeSpan.FromSeconds(60);

// Читаем промпт из файла при старте
var promptPath = Path.Combine(AppContext.BaseDirectory, "Prompts", "SystemPrompt.txt");
_systemPromptTemplate = File.Exists(promptPath)
? File.ReadAllText(promptPath)
: GetFallbackPrompt(); // если файл не найден — используем запасной
}

public async Task<string> AnalyzeTextAsync(string text, string language = "ru")
public async Task<string> AnalyzeTextAsync(
string text,
string language = "ru",
string? userPrompt = null) // ← опциональный промпт от пользователя
{
var langInstruction = language switch
{
"ru" => "Текст на русском языке. Строго следуй правилам русской орфографии, пунктуации и грамматики.",
"en" => "Текст на английском языке. Строго следуй правилам английской орфографии и грамматики.",
"mixed" => """
Текст содержит русский и английский языки одновременно.
Англицизмы которые стали общепринятыми (например: интернет, файл, сайт) — допустимы.
Проверяй каждый язык по его собственным правилам.
""",
"mixed" => "Текст содержит русский и английский языки. Общепринятые англицизмы допустимы. Проверяй каждый язык по его правилам.",
_ => "Определи язык текста самостоятельно и применяй соответствующие правила."
};

var systemPrompt = $$$"""
Ты — профессиональный редактор текста с глубоким знанием русского и английского языков.

{langInstruction}

Твоя задача:
1. Найти орфографические ошибки
2. Найти пунктуационные ошибки
3. Найти грамматические ошибки
4. Найти тавтологии и смысловые повторы
5. Найти стилистические проблемы
6. Найти канцеляризмы и штампы
7. Учитывать КОНТЕКСТ предложения — одинаково написанные слова могут иметь разный смысл
8. Посчитать статистику текста

ВАЖНО: Верни ТОЛЬКО валидный JSON без пояснений, без markdown, без ```json.
Формат ответа:
{{
"issues": [
{{
"category": "Орфография|Пунктуация|Грамматика|Тавтология|Стиль|Канцеляризм",
"original": "исходный фрагмент из текста",
"suggestion": "предлагаемая замена",
"description": "краткое объяснение проблемы"
}}
],
"statistics": {{
"wordCount": 0,
"sentenceCount": 0,
"readabilityScore": 0,
"waterScore": 0
}}
}}
// Подставляем язык в шаблон
var systemPrompt = _systemPromptTemplate
.Replace("{LANG_INSTRUCTION}", langInstruction);

// Если пользователь задал свои требования — добавляем их
if (!string.IsNullOrWhiteSpace(userPrompt))
{
systemPrompt += $"""


ДОПОЛНИТЕЛЬНЫЕ ТРЕБОВАНИЯ ПОЛЬЗОВАТЕЛЯ:
{userPrompt}

Эти требования имеют приоритет над общими правилами.
""";
}

try
{
var requestBody = new
{
model = _model,
temperature = 0.1, // TEMPERATURE MODELS (0.1 = focused and deterministic, 0.9 = creative and random)
temperature = 0.1,
messages = new[]
{
new { role = "system", content = systemPrompt },
new { role = "user", content = text }
new {
role = "user",
content = $"""
Проанализируй текст. Помни: category должна быть СТРОГО одним словом из списка: Орфография, Пунктуация, Грамматика, Тавтология, Стиль, Канцеляризм.

ТЕКСТ:
{text}
"""
}
}
};

var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");

var response = await _httpClient.PostAsync("chat/completions", content);
var responseJson = await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
{
_logger.LogError("Groq API error: {StatusCode} {Body}", response.StatusCode, responseJson);
_logger.LogError("Groq error: {Code} {Body}", response.StatusCode, responseJson);
throw new Exception($"API вернул ошибку: {response.StatusCode}");
}

Expand All @@ -103,19 +98,27 @@ 8. Посчитать статистику текста
.GetProperty("content")
.GetString();

// DELETE MARKDOWN
resultText = resultText?
return resultText?
.Replace("```json", "")
.Replace("```", "")
.Trim();

return resultText ?? "{}";
.Trim() ?? "{}";
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при обращении к Groq API");
_logger.LogError(ex, "Ошибка Groq API");
throw;
}
}

// Запасной промпт если файл не найден
private static string GetFallbackPrompt() => """
Ты — профессиональный редактор текста.
ЯЗЫК: {LANG_INSTRUCTION}
Найди все ошибки и верни ТОЛЬКО валидный JSON:
{
"issues": [{"category": "...", "original": "...", "suggestion": "...", "description": "..."}],
"statistics": {"wordCount": 0, "sentenceCount": 0, "readabilityScore": 0, "waterScore": 0}
}
""";
}
}
43 changes: 43 additions & 0 deletions Services/Promts/SystemPrompt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Ты — профессиональный редактор текста. Анализируй строго и возвращай ТОЛЬКО валидный JSON.

ЯЗЫК: {LANG_INSTRUCTION}

КАТЕГОРИИ — используй СТРОГО одну из списка, никаких комбинаций:
- Орфография — опечатки, неверное написание слов
- Пунктуация — запятые, точки, тире, знаки препинания
- Грамматика — согласование слов, падежи, глагольные формы
- Тавтология — повтор одного слова или смысла рядом
- Стиль — нарушение сочетаемости, смешение стилей, длинные предложения
- Канцеляризм — бюрократические штампы, отглагольные существительные

ПРАВИЛА:
1. Каждая проблема — отдельный объект в массиве
2. Если проблема затрагивает и орфографию и пунктуацию — создай ДВА отдельных объекта
3. "category" — ТОЛЬКО одно слово из списка выше. Никаких "Орфография и пунктуация", "Орфография/Пунктуация" и подобного
4. "original" — точный фрагмент из текста, который надо исправить
5. "suggestion" — исправленный вариант
6. "description" — краткое объяснение на русском, 1 предложение

СТАТИСТИКА:
- wordCount: количество слов
- sentenceCount: количество предложений
- readabilityScore: от 0 до 100, где 100 = очень легко читать
- waterScore: процент воды и штампов от 0 до 100

ФОРМАТ — верни ТОЛЬКО это, без markdown, без пояснений:
{
"issues": [
{
"category": "Орфография",
"original": "точный фрагмент",
"suggestion": "исправление",
"description": "объяснение"
}
],
"statistics": {
"wordCount": 0,
"sentenceCount": 0,
"readabilityScore": 0,
"waterScore": 0
}
}
6 changes: 6 additions & 0 deletions WebEditor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@
<PackageReference Include="QuestPDF" Version="2026.2.4" />
</ItemGroup>

<ItemGroup>
<None Update="Services\Promts\SystemPrompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
49 changes: 22 additions & 27 deletions client/src/App.css
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
.App {
text-align: center;
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

.App-logo {
height: 40vmin;
pointer-events: none;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: background-color 0.3s, color 0.3s;
}

@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
.issue-highlight {
border-radius: 3px !important;
padding: 1px 2px !important;
cursor: pointer !important;
}

.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
::-webkit-scrollbar {
width: 6px;
}

.App-link {
color: #61dafb;
::-webkit-scrollbar-track {
background: transparent;
}

@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
::-webkit-scrollbar-thumb {
background: rgba(91, 155, 213, 0.3);
border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
background: rgba(91, 155, 213, 0.5);
}
Loading
Loading