Создать ссылку
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ссылка готова
Открывается публичная страница с карточками — без входа и выбора параметров. Ссылку можно изменить или удалить в разделе «Ссылки».
-
-
diff --git a/public/js/journal.js b/public/js/journal.js
index 8775159..488ff8e 100644
--- a/public/js/journal.js
+++ b/public/js/journal.js
@@ -8,6 +8,7 @@ let page = 1;
let totalEntries = 0;
let searchTimer = null;
let view = localStorage.getItem('journalView') === 'cards' ? 'cards' : 'list';
+let aiCorrecting = false;
function isImageFile(name) {
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
@@ -190,6 +191,53 @@ function closeEdit() {
editId = null;
}
+async function aiCorrect() {
+ if (aiCorrecting) return;
+ const textarea = document.getElementById('editDesc');
+ const text = textarea.value.trim();
+ if (!text) { showToast('Нет текста для исправления'); return; }
+ const btn = document.getElementById('aiCorrectBtn');
+ const modal = document.getElementById('aiCorrectModal');
+ const suggestionEl = document.getElementById('aiSuggestion');
+ aiCorrecting = true;
+ btn.disabled = true;
+ btn.textContent = '⏳';
+ suggestionEl.textContent = 'Обработка...';
+ modal.classList.add('open');
+ try {
+ const res = await fetch(`${API}/api/ai/correct`, {
+ method: 'POST',
+ headers: hdrJson(),
+ body: JSON.stringify({ text })
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ throw new Error(err.error || 'Ошибка ИИ');
+ }
+ const data = await res.json();
+ suggestionEl.textContent = data.suggestion || text;
+ } catch (e) {
+ suggestionEl.textContent = 'Ошибка: ' + e.message;
+ showToast('ИИ недоступен: ' + e.message);
+ } finally {
+ aiCorrecting = false;
+ btn.disabled = false;
+ btn.textContent = '✨';
+ }
+}
+
+function closeAiCorrect() {
+ document.getElementById('aiCorrectModal').classList.remove('open');
+}
+
+function acceptAiCorrect() {
+ const suggestion = document.getElementById('aiSuggestion').textContent;
+ if (suggestion && suggestion !== 'Обработка...' && !suggestion.startsWith('Ошибка:')) {
+ document.getElementById('editDesc').value = suggestion;
+ }
+ closeAiCorrect();
+}
+
async function saveEdit() {
if (!editId) return;
const name = document.getElementById('editName').value.trim();
@@ -209,7 +257,7 @@ async function saveEdit() {
for (const f of files) formData.append('files', f);
const uploadRes = await fetch(`${API}/api/entries/${editId}/files`, {
method: 'POST',
- headers: { 'X-Admin-Token': token },
+ headers: hdr(),
body: formData
});
if (!uploadRes.ok) {
@@ -388,6 +436,13 @@ document.getElementById('editFilesPreview').addEventListener('click', e => {
if (btn) removeEditFile(parseInt(btn.dataset.removeFile, 10));
});
+document.getElementById('aiCorrectBtn').addEventListener('click', aiCorrect);
+document.getElementById('aiAcceptBtn').addEventListener('click', acceptAiCorrect);
+document.getElementById('aiRejectBtn').addEventListener('click', closeAiCorrect);
+document.getElementById('aiCorrectModal').addEventListener('click', e => {
+ if (e.target === e.currentTarget) closeAiCorrect();
+});
+
document.getElementById('exportCsvBtn').addEventListener('click', exportCSV);
document.getElementById('createLinkBtn').addEventListener('click', openLinkCreate);
document.querySelectorAll('#viewToggle button').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
diff --git a/server.js b/server.js
index b947300..2e662ff 100644
--- a/server.js
+++ b/server.js
@@ -621,6 +621,32 @@ const gunzipAsync = require('util').promisify(require('zlib').gunzip);
const zlib = require('zlib');
const tar = require('tar');
const os = require('os');
+const AI_URL = process.env.AI_URL || 'http://text-corrector:8080';
+
+async function aiCorrectText(text) {
+ try {
+ const res = await fetch(`${AI_URL}/v1/chat/completions`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model: 'qwen2.5-1.5b-instruct',
+ messages: [
+ { role: 'system', content: 'Ты — редактор текстов для педагогического журнала. Исправь грамматические, орфографические и пунктуационные ошибки. Сохрани смысл и стиль. Верни ТОЛЬКО исправленный текст, без пояснений.' },
+ { role: 'user', content: text }
+ ],
+ temperature: 0.1,
+ max_tokens: 512
+ })
+ });
+ if (!res.ok) throw new Error(`AI service error: ${res.status}`);
+ const data = await res.json();
+ return data.choices?.[0]?.message?.content?.trim() || text;
+ } catch (e) {
+ console.error('AI correct error:', e);
+ throw e;
+ }
+}
+
const uploadBackup = multer({
storage: multer.diskStorage({
destination: (_, __, cb) => {
@@ -2248,6 +2274,17 @@ app.delete('/api/entries/:id/permanent', requireAuth, async (req, res) => {
res.json({ ok: true });
});
+app.post('/api/ai/correct', requireAuth, async (req, res) => {
+ const text = reqStr(req.body?.text, 5000);
+ if (!text) return res.status(400).json({ error: 'Текст не указан' });
+ try {
+ const corrected = await aiCorrectText(text);
+ res.json({ suggestion: corrected });
+ } catch (e) {
+ res.status(502).json({ error: 'Сервис ИИ недоступен: ' + e.message });
+ }
+});
+
app.get('/api/trash', requireAuth, async (req, res) => {
const { limit, offset } = req.query;
const bw = branchScope(req.user);