initial commit

This commit is contained in:
2026-02-28 19:44:58 +01:00
commit c1fbcdf6b1
13 changed files with 2422 additions and 0 deletions

BIN
src/public/favicon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/public/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/public/favicon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

34
src/public/favicon.svg Normal file
View File

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4a6cf7"/>
<stop offset="100%" style="stop-color:#6c8eff"/>
</linearGradient>
<linearGradient id="globe" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:0.95"/>
<stop offset="100%" style="stop-color:#e0e7ff;stop-opacity:0.9"/>
</linearGradient>
</defs>
<!-- Rounded square background -->
<rect width="512" height="512" rx="112" fill="url(#bg)"/>
<!-- Globe circle -->
<circle cx="232" cy="272" r="140" fill="none" stroke="url(#globe)" stroke-width="14"/>
<!-- Globe meridians -->
<ellipse cx="232" cy="272" rx="60" ry="140" fill="none" stroke="url(#globe)" stroke-width="10"/>
<ellipse cx="232" cy="272" rx="110" ry="140" fill="none" stroke="url(#globe)" stroke-width="8" opacity="0.5"/>
<!-- Globe parallels -->
<line x1="92" y1="220" x2="372" y2="220" stroke="url(#globe)" stroke-width="9" opacity="0.7"/>
<line x1="92" y1="272" x2="372" y2="272" stroke="url(#globe)" stroke-width="9"/>
<line x1="92" y1="324" x2="372" y2="324" stroke="url(#globe)" stroke-width="9" opacity="0.7"/>
<!-- Speech bubble -->
<rect x="270" y="80" width="170" height="110" rx="24" fill="white" opacity="0.95"/>
<polygon points="290,190 310,190 280,220" fill="white" opacity="0.95"/>
<!-- "de" text in speech bubble -->
<text x="355" y="152" font-family="Arial, Helvetica, sans-serif" font-size="62" font-weight="700" fill="#4a6cf7" text-anchor="middle">de</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

1112
src/public/index.html Normal file

File diff suppressed because it is too large Load Diff

79
src/server.js Normal file
View File

@@ -0,0 +1,79 @@
const express = require("express");
const path = require("path");
const app = express();
const PORT = process.env.PORT || 8083;
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));
const LEVEL_DESCRIPTIONS = {
A2: "базовый (A2): будь снисходителен к ошибкам, хвали за попытку, указывай только грубые ошибки",
B1: "средний (B1): указывай основные грамматические ошибки и предлагай улучшения",
B2: "выше среднего (B2): анализируй детально — грамматику, стиль, лексику, естественность речи",
};
app.post("/api/check", async (req, res) => {
const { question, answer, level } = req.body;
if (!question || !answer) {
return res.status(400).json({ error: "question and answer are required" });
}
if (!ANTHROPIC_API_KEY) {
return res.status(500).json({ error: "API key not configured" });
}
const levelDesc = LEVEL_DESCRIPTIONS[level] || LEVEL_DESCRIPTIONS["B1"];
const systemPrompt = `Ты помощник для практики разговорного немецкого языка. Уровень пользователя: ${levelDesc}.
ВАЖНО: ответ пользователя получен через автоматическое распознавание речи (speech-to-text), поэтому в нём могут быть неправильные заглавные буквы, отсутствовать знаки препинания или быть опечатки — это артефакты распознавания, НЕ ошибки пользователя. Никогда не упоминай и не исправляй заглавные буквы, пунктуацию и орфографию.
Тебе дают вопрос на немецком и ответ пользователя на немецком. Твоя задача:
1. **Оценка** — кратко оцени ответ (1-2 предложения)
2. **Ошибки** — перечисли грамматические/лексические ошибки с исправлениями (если есть)
3. **Улучшенная версия** — предложи более естественную/правильную формулировку ответа
4. **Полезные слова и фразы** — 2-4 слова или выражения по теме с переводом
Отвечай на русском языке. Будь конструктивным. Используй markdown для форматирования.`;
const userMessage = `Вопрос: ${question}\n\nОтвет пользователя: ${answer}`;
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-haiku-4-5-20251001",
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
}),
});
if (!response.ok) {
const error = await response.text();
console.error("Anthropic API error:", response.status, error);
return res.status(502).json({ error: "AI service error" });
}
const data = await response.json();
const feedback = data.content?.[0]?.text || "";
res.json({ feedback });
} catch (err) {
console.error("Request failed:", err);
res.status(500).json({ error: "Internal server error" });
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});