Compare commits
No commits in common. "1d4cf84d6daa95b1ab85787b315818bf4b4c159d" and "300e881245c1e9ad3946111251387ad30d785baa" have entirely different histories.
1d4cf84d6d
...
300e881245
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -158,8 +158,8 @@
|
||||
|
||||
<!-- Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<script type="module" crossorigin src="/assets/index-Tw4PJyEU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C3lVJ81m.css">
|
||||
<script type="module" crossorigin src="/assets/index-vvy_XuzQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DK8OUj6L.css">
|
||||
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
||||
<body>
|
||||
<div id="root">
|
||||
|
||||
@ -1 +1 @@
|
||||
if(!self.define){let e,n={};const i=(i,c)=>(i=new URL(i+".js",c).href,n[i]||new Promise(n=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=n,document.head.appendChild(e)}else e=i,importScripts(i),n()}).then(()=>{let e=n[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(c,s)=>{const a=e||("document"in self?document.currentScript.src:"")||location.href;if(n[a])return;let o={};const r=e=>i(e,a),d={module:{uri:a},exports:o,require:r};n[a]=Promise.all(c.map(e=>d[e]||r(e))).then(e=>(s(...e),o))}}define(["./workbox-40c80ae4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-C3lVJ81m.css",revision:"b7d7a06e04415f937626bf2a2fa0e494"},{url:"assets/index-Tw4PJyEU.js",revision:"258806cb29e8a6a43d2fe456136dffbf"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"81dcb6a6346bcee9f6f569316322388e"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api/,/^\/uploads/]})),e.registerRoute(({request:e})=>"navigate"===e.mode,new e.CacheFirst({cacheName:"pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/\.html$/,new e.CacheFirst({cacheName:"html-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});
|
||||
if(!self.define){let e,n={};const i=(i,c)=>(i=new URL(i+".js",c).href,n[i]||new Promise(n=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=n,document.head.appendChild(e)}else e=i,importScripts(i),n()}).then(()=>{let e=n[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(c,s)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(n[o])return;let a={};const r=e=>i(e,o),d={module:{uri:o},exports:a,require:r};n[o]=Promise.all(c.map(e=>d[e]||r(e))).then(e=>(s(...e),a))}}define(["./workbox-40c80ae4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-DK8OUj6L.css",revision:"b1e2c4e8724be2f2bcee585338910e99"},{url:"assets/index-vvy_XuzQ.js",revision:"054024cdea842a8b5c1681d0e44680fa"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"10119735705f0553541d8ce5fe13c02e"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api/,/^\/uploads/]})),e.registerRoute(({request:e})=>"navigate"===e.mode,new e.CacheFirst({cacheName:"pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/\.html$/,new e.CacheFirst({cacheName:"html-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});
|
||||
|
||||
1
backend/public/workbox-40c80ae4.js
Normal file
1
backend/public/workbox-40c80ae4.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/workbox-57555046.js
Normal file
1
backend/public/workbox-57555046.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/public/workbox-e20531c6.js
Normal file
1
backend/public/workbox-e20531c6.js
Normal file
File diff suppressed because one or more lines are too long
@ -2423,7 +2423,7 @@ app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
|
||||
try {
|
||||
// Получаем AI настройки пользователя
|
||||
const getSettingsSql =
|
||||
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||||
"SELECT openai_api_key, openai_base_url, openai_model FROM users WHERE id = ?";
|
||||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения AI настроек:", err.message);
|
||||
@ -2441,13 +2441,6 @@ app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
|
||||
.json({ error: "Настройте AI настройки в параметрах" });
|
||||
}
|
||||
|
||||
// Проверяем, включены ли функции ИИ
|
||||
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Функции ИИ отключены в настройках" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Парсим URL
|
||||
const url = new URL(settings.openai_base_url);
|
||||
@ -2553,356 +2546,6 @@ app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// API для генерации тегов через AI
|
||||
app.post("/api/ai/generate-tags", requireApiAuth, async (req, res) => {
|
||||
const { text } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: "Текст обязателен" });
|
||||
}
|
||||
|
||||
// Проверяем минимальную длину текста
|
||||
if (text.trim().length < 10) {
|
||||
return res.status(400).json({
|
||||
error: "Текст слишком короткий для генерации тегов. Минимум 10 символов.",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем AI настройки пользователя
|
||||
const getSettingsSql =
|
||||
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения AI настроек:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (
|
||||
!settings ||
|
||||
!settings.openai_api_key ||
|
||||
!settings.openai_base_url ||
|
||||
!settings.openai_model
|
||||
) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Настройте AI настройки в параметрах" });
|
||||
}
|
||||
|
||||
// Проверяем, включены ли функции ИИ
|
||||
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Функции ИИ отключены в настройках" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Парсим URL
|
||||
const url = new URL(settings.openai_base_url);
|
||||
const isHttps = url.protocol === "https:";
|
||||
const hostname = url.hostname;
|
||||
const port = url.port || (isHttps ? 443 : 80);
|
||||
// Формируем путь, избегая дублирования
|
||||
let path = url.pathname || "";
|
||||
if (!path.endsWith("/chat/completions")) {
|
||||
path = path.endsWith("/")
|
||||
? path + "chat/completions"
|
||||
: path + "/chat/completions";
|
||||
}
|
||||
|
||||
// Подготавливаем данные для запроса
|
||||
const requestBody = {
|
||||
model: settings.openai_model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Ты помощник для генерации тегов. Проанализируй текст и предложи 3-8 релевантных тегов на русском языке через запятую. Теги должны быть краткими (1-3 слова). Избегай общих слов типа 'текст', 'заметка', 'информация'. Верни ТОЛЬКО теги через запятую, без знаков #, без нумерации, без точек. Пример: работа, проект, задачи, дедлайн",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: text.substring(0, 2000), // Ограничиваем длину текста
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 200,
|
||||
};
|
||||
|
||||
const requestData = JSON.stringify(requestBody);
|
||||
|
||||
console.log("Отправляем запрос к AI API:");
|
||||
console.log("URL:", settings.openai_base_url);
|
||||
console.log("Модель:", settings.openai_model);
|
||||
console.log("Длина текста:", text.length);
|
||||
console.log(
|
||||
"Запрос (без ключа):",
|
||||
JSON.stringify(
|
||||
{
|
||||
...requestBody,
|
||||
messages: requestBody.messages.map((m) => ({
|
||||
...m,
|
||||
content:
|
||||
m.content.length > 100
|
||||
? m.content.substring(0, 100) + "..."
|
||||
: m.content,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
// Выполняем HTTP запрос
|
||||
const tagsResponse = await new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
path: path,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${settings.openai_api_key}`,
|
||||
"Content-Length": Buffer.byteLength(requestData),
|
||||
},
|
||||
};
|
||||
|
||||
const client = isHttps ? https : http;
|
||||
const req = client.request(options, (res) => {
|
||||
let data = "";
|
||||
|
||||
res.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on("end", () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
const responseData = JSON.parse(data);
|
||||
console.log(
|
||||
"Полный ответ от AI API:",
|
||||
JSON.stringify(responseData, null, 2)
|
||||
);
|
||||
|
||||
// Проверяем различные форматы ответа
|
||||
let tagsText = "";
|
||||
|
||||
// Стандартный формат OpenAI
|
||||
if (
|
||||
responseData.choices &&
|
||||
responseData.choices[0] &&
|
||||
responseData.choices[0].message
|
||||
) {
|
||||
tagsText = responseData.choices[0].message.content || "";
|
||||
}
|
||||
// Альтернативный формат (некоторые API)
|
||||
else if (
|
||||
responseData.choices &&
|
||||
responseData.choices[0] &&
|
||||
responseData.choices[0].text
|
||||
) {
|
||||
tagsText = responseData.choices[0].text || "";
|
||||
}
|
||||
// Прямой content в корне (некоторые API)
|
||||
else if (responseData.content) {
|
||||
tagsText = responseData.content || "";
|
||||
}
|
||||
// Прямой text в корне (некоторые API)
|
||||
else if (responseData.text) {
|
||||
tagsText = responseData.text || "";
|
||||
}
|
||||
// Пробуем найти любой text или content в ответе
|
||||
else {
|
||||
const responseString = JSON.stringify(responseData);
|
||||
console.log(
|
||||
"Пробуем найти content/text в ответе:",
|
||||
responseString
|
||||
);
|
||||
// Если ничего не нашли, пробуем извлечь из ответа
|
||||
if (
|
||||
responseData.choices &&
|
||||
responseData.choices.length > 0
|
||||
) {
|
||||
const firstChoice = responseData.choices[0];
|
||||
tagsText =
|
||||
firstChoice.message?.content ||
|
||||
firstChoice.text ||
|
||||
firstChoice.content ||
|
||||
firstChoice.message?.text ||
|
||||
"";
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Извлеченный текст тегов:", tagsText);
|
||||
console.log("Длина текста:", tagsText.length);
|
||||
|
||||
if (!tagsText || tagsText.trim().length === 0) {
|
||||
console.error("Пустой content в ответе AI:", responseData);
|
||||
console.error(
|
||||
"Структура ответа:",
|
||||
Object.keys(responseData)
|
||||
);
|
||||
if (responseData.choices) {
|
||||
console.error(
|
||||
"Choices структура:",
|
||||
JSON.stringify(responseData.choices, null, 2)
|
||||
);
|
||||
}
|
||||
reject(
|
||||
new Error(
|
||||
"ИИ вернул пустой ответ. Проверьте настройки модели и попробуйте еще раз."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(tagsText);
|
||||
} catch (err) {
|
||||
console.error("Ошибка парсинга ответа:", err);
|
||||
console.error("Данные ответа:", data);
|
||||
reject(
|
||||
new Error("Ошибка обработки ответа от AI: " + err.message)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error("Ошибка OpenAI API:", res.statusCode, data);
|
||||
let errorMessage = `Ошибка OpenAI API: ${res.statusCode}`;
|
||||
try {
|
||||
const errorData = JSON.parse(data);
|
||||
if (errorData.error && errorData.error.message) {
|
||||
errorMessage += " - " + errorData.error.message;
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage += " - " + data.substring(0, 200);
|
||||
}
|
||||
reject(new Error(errorMessage));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", (error) => {
|
||||
console.error("Ошибка запроса к OpenAI:", error);
|
||||
reject(new Error("Ошибка подключения к OpenAI API"));
|
||||
});
|
||||
|
||||
req.write(requestData);
|
||||
req.end();
|
||||
});
|
||||
|
||||
// Логируем сырой ответ от AI для отладки
|
||||
console.log("AI ответ для генерации тегов:", tagsResponse);
|
||||
console.log("Тип ответа:", typeof tagsResponse);
|
||||
console.log("Длина ответа:", tagsResponse ? tagsResponse.length : 0);
|
||||
|
||||
// Проверяем, что ответ не пустой
|
||||
if (
|
||||
!tagsResponse ||
|
||||
typeof tagsResponse !== "string" ||
|
||||
tagsResponse.trim().length === 0
|
||||
) {
|
||||
console.error("AI вернул пустой ответ");
|
||||
return res.status(500).json({
|
||||
error:
|
||||
"ИИ вернул пустой ответ. Попробуйте еще раз или проверьте настройки AI.",
|
||||
});
|
||||
}
|
||||
|
||||
// Парсим теги из ответа
|
||||
// Пробуем разные разделители: запятая, точка с запятой, перенос строки
|
||||
let tags = [];
|
||||
const cleanResponse = tagsResponse.trim();
|
||||
|
||||
// Сначала пробуем разделить по запятой
|
||||
if (cleanResponse.includes(",")) {
|
||||
tags = cleanResponse.split(",");
|
||||
}
|
||||
// Затем пробуем точку с запятой
|
||||
else if (cleanResponse.includes(";")) {
|
||||
tags = cleanResponse.split(";");
|
||||
}
|
||||
// Затем пробуем перенос строки
|
||||
else if (cleanResponse.includes("\n")) {
|
||||
tags = cleanResponse.split("\n");
|
||||
}
|
||||
// Если ничего не найдено, пробуем пробел (как последний вариант)
|
||||
else {
|
||||
tags = cleanResponse.split(/\s+/);
|
||||
}
|
||||
|
||||
console.log("Теги до обработки:", tags);
|
||||
|
||||
// Обрабатываем теги
|
||||
tags = tags
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.length > 0)
|
||||
.map((tag) => {
|
||||
// Убираем # в начале и конце
|
||||
tag = tag.replace(/^#+\s*/, "").replace(/\s*#+$/, "");
|
||||
// Убираем точки, дефисы в начале/конце если они есть
|
||||
tag = tag.replace(/^[.\-\s]+|[.\-\s]+$/g, "");
|
||||
// Убираем кавычки если есть
|
||||
tag = tag.replace(/^["']+|["']+$/g, "");
|
||||
return tag.trim();
|
||||
})
|
||||
.filter((tag) => {
|
||||
// Фильтруем слишком короткие и слишком длинные теги
|
||||
return tag.length > 0 && tag.length <= 50;
|
||||
})
|
||||
.filter((tag) => {
|
||||
// Фильтруем общие слова, которые не являются тегами
|
||||
const commonWords = [
|
||||
"текст",
|
||||
"заметка",
|
||||
"информация",
|
||||
"данные",
|
||||
"файл",
|
||||
"документ",
|
||||
];
|
||||
return !commonWords.includes(tag.toLowerCase());
|
||||
})
|
||||
.slice(0, 10); // Максимум 10 тегов
|
||||
|
||||
console.log("Распарсенные теги после обработки:", tags);
|
||||
|
||||
// Если тегов нет, это тоже ошибка
|
||||
if (!tags || tags.length === 0) {
|
||||
console.error(
|
||||
"Не удалось распарсить теги из ответа AI. Исходный ответ:",
|
||||
tagsResponse
|
||||
);
|
||||
return res.status(500).json({
|
||||
error:
|
||||
"ИИ вернул ответ, но не удалось извлечь теги. Попробуйте еще раз или добавьте больше текста в заметку.",
|
||||
});
|
||||
}
|
||||
|
||||
// Логируем использование AI
|
||||
logAction(
|
||||
req.session.userId,
|
||||
"ai_generate_tags",
|
||||
`Сгенерированы теги через AI: ${tags.length} тегов`
|
||||
);
|
||||
|
||||
res.json({ success: true, tags });
|
||||
} catch (error) {
|
||||
console.error("Ошибка вызова OpenAI API:", error);
|
||||
console.error("Стек ошибки:", error.stack);
|
||||
return res.status(500).json({
|
||||
error: error.message || "Ошибка подключения к OpenAI API",
|
||||
details:
|
||||
process.env.NODE_ENV === "development" ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка генерации тегов:", error);
|
||||
console.error("Стек ошибки:", error.stack);
|
||||
res.status(500).json({
|
||||
error: error.message || "Ошибка генерации тегов",
|
||||
details: process.env.NODE_ENV === "development" ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API для объединения заметок через AI
|
||||
app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
|
||||
const { notes } = req.body;
|
||||
@ -2916,7 +2559,7 @@ app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
|
||||
try {
|
||||
// Получаем AI настройки пользователя
|
||||
const getSettingsSql =
|
||||
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||||
"SELECT openai_api_key, openai_base_url, openai_model FROM users WHERE id = ?";
|
||||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения AI настроек:", err.message);
|
||||
@ -2934,13 +2577,6 @@ app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
|
||||
.json({ error: "Настройте AI настройки в параметрах" });
|
||||
}
|
||||
|
||||
// Проверяем, включены ли функции ИИ
|
||||
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Функции ИИ отключены в настройках" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Парсим URL
|
||||
const url = new URL(settings.openai_base_url);
|
||||
|
||||
@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "/index.html",
|
||||
"revision": "0.tafmhe7064g"
|
||||
"revision": "0.u6qfhq29adg"
|
||||
}], {
|
||||
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
||||
});
|
||||
|
||||
@ -16,12 +16,4 @@ export const aiApi = {
|
||||
);
|
||||
return data.mergedText;
|
||||
},
|
||||
|
||||
generateTags: async (text: string): Promise<string[]> => {
|
||||
const { data } = await axiosClient.post<{ tags: string[] }>(
|
||||
"/ai/generate-tags",
|
||||
{ text }
|
||||
);
|
||||
return data.tags;
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,265 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNotification } from "../../hooks/useNotification";
|
||||
|
||||
interface GenerateTagsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSelectTags: (tags: string[]) => void;
|
||||
suggestedTags: string[];
|
||||
existingTags: string[];
|
||||
isLoading?: boolean;
|
||||
hasError?: boolean;
|
||||
}
|
||||
|
||||
export const GenerateTagsModal: React.FC<GenerateTagsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSelectTags,
|
||||
suggestedTags,
|
||||
existingTags,
|
||||
isLoading = false,
|
||||
hasError = false,
|
||||
}) => {
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// При открытии модалки, выбираем только новые теги (которых еще нет в заметке)
|
||||
const newTags = suggestedTags.filter(
|
||||
(tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase())
|
||||
);
|
||||
setSelectedTags(newTags);
|
||||
} else {
|
||||
// При закрытии сбрасываем выбор
|
||||
setSelectedTags([]);
|
||||
}
|
||||
}, [isOpen, suggestedTags, existingTags]);
|
||||
|
||||
const handleTagToggle = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
if (prev.includes(tag)) {
|
||||
return prev.filter((t) => t !== tag);
|
||||
} else {
|
||||
return [...prev, tag];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const newTags = suggestedTags.filter(
|
||||
(tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase())
|
||||
);
|
||||
setSelectedTags(newTags);
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
setSelectedTags([]);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
if (selectedTags.length === 0) {
|
||||
showNotification("Выберите хотя бы один тег", "warning");
|
||||
return;
|
||||
}
|
||||
onSelectTags(selectedTags);
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
}
|
||||
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const newTags = suggestedTags.filter(
|
||||
(tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase())
|
||||
);
|
||||
const existingInSuggested = suggestedTags.filter((tag) =>
|
||||
existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="modal" style={{ display: "block" }} onClick={onClose}>
|
||||
<div
|
||||
className="modal-content"
|
||||
style={{ maxWidth: "600px" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h3>Выберите теги</h3>
|
||||
<span className="modal-close" onClick={onClose}>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: "center", padding: "40px 20px" }}>
|
||||
<div className="loading-spinner" style={{ margin: "0 auto 20px" }}></div>
|
||||
<p>Генерирую теги через ИИ...</p>
|
||||
</div>
|
||||
) : hasError ? (
|
||||
<div style={{ textAlign: "center", padding: "40px 20px" }}>
|
||||
<p style={{ color: "#dc3545", marginBottom: "10px" }}>
|
||||
Не удалось сгенерировать теги
|
||||
</p>
|
||||
<p style={{ fontSize: "14px", color: "#666" }}>
|
||||
Произошла ошибка при генерации тегов. Проверьте настройки AI или попробуйте еще раз.
|
||||
</p>
|
||||
</div>
|
||||
) : suggestedTags.length === 0 ? (
|
||||
<div style={{ textAlign: "center", padding: "40px 20px" }}>
|
||||
<p style={{ color: "#dc3545", marginBottom: "10px" }}>
|
||||
Не удалось сгенерировать теги
|
||||
</p>
|
||||
<p style={{ fontSize: "14px", color: "#666" }}>
|
||||
ИИ не смог предложить теги для этой заметки. Попробуйте еще раз или добавьте больше текста.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{newTags.length > 0 && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: 0, fontSize: "16px" }}>Предлагаемые теги:</h4>
|
||||
<div style={{ display: "flex", gap: "10px" }}>
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={handleSelectAll}
|
||||
style={{ fontSize: "12px", padding: "5px 10px" }}
|
||||
>
|
||||
Выбрать все
|
||||
</button>
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={handleDeselectAll}
|
||||
style={{ fontSize: "12px", padding: "5px 10px" }}
|
||||
>
|
||||
Снять все
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "10px",
|
||||
marginBottom: "20px",
|
||||
padding: "15px",
|
||||
border: "1px solid var(--border-color)",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
minHeight: "60px",
|
||||
}}
|
||||
>
|
||||
{newTags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => handleTagToggle(tag)}
|
||||
className={`tag ${selectedTags.includes(tag) ? "active" : ""}`}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "20px",
|
||||
border: selectedTags.includes(tag)
|
||||
? "2px solid var(--accent-color)"
|
||||
: "1px solid var(--border-color)",
|
||||
backgroundColor: selectedTags.includes(tag)
|
||||
? "var(--accent-color)"
|
||||
: "var(--bg-tertiary)",
|
||||
color: selectedTags.includes(tag) ? "#fff" : "var(--text-primary)",
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{existingInSuggested.length > 0 && (
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
<h4 style={{ margin: "0 0 10px 0", fontSize: "14px", color: "#666" }}>
|
||||
Теги, которые уже есть в заметке:
|
||||
</h4>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "10px",
|
||||
padding: "10px",
|
||||
backgroundColor: "var(--bg-tertiary)",
|
||||
borderRadius: "8px",
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{existingInSuggested.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="tag"
|
||||
style={{
|
||||
padding: "6px 12px",
|
||||
borderRadius: "20px",
|
||||
border: "1px solid var(--border-color)",
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
color: "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTags.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: "20px",
|
||||
padding: "10px",
|
||||
backgroundColor: "var(--bg-hover)",
|
||||
borderRadius: "8px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
<strong>Будет добавлено тегов: {selectedTags.length}</strong>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleApply}
|
||||
disabled={isLoading || selectedTags.length === 0}
|
||||
>
|
||||
Применить ({selectedTags.length})
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={onClose} disabled={isLoading}>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,162 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface ImproveTextModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onApply: (improvedText: string) => void;
|
||||
originalText: string;
|
||||
improvedText: string;
|
||||
isLoading?: boolean;
|
||||
hasError?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const ImproveTextModal: React.FC<ImproveTextModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onApply,
|
||||
originalText,
|
||||
improvedText,
|
||||
isLoading = false,
|
||||
hasError = false,
|
||||
errorMessage,
|
||||
}) => {
|
||||
const [localImprovedText, setLocalImprovedText] = useState(improvedText);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLocalImprovedText(improvedText);
|
||||
}
|
||||
}, [isOpen, improvedText]);
|
||||
|
||||
const handleApply = () => {
|
||||
if (!localImprovedText.trim()) {
|
||||
return;
|
||||
}
|
||||
onApply(localImprovedText);
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
}
|
||||
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal" style={{ display: "block" }} onClick={onClose}>
|
||||
<div
|
||||
className="modal-content"
|
||||
style={{ maxWidth: "800px" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h3>Улучшенный текст</h3>
|
||||
<span className="modal-close" onClick={onClose}>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
<div className="modal-body" style={{ maxHeight: "70vh", overflowY: "auto" }}>
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: "center", padding: "40px 20px" }}>
|
||||
<div className="loading-spinner" style={{ margin: "0 auto 20px" }}></div>
|
||||
<p>Улучшаю текст через ИИ...</p>
|
||||
</div>
|
||||
) : hasError ? (
|
||||
<div style={{ textAlign: "center", padding: "40px 20px" }}>
|
||||
<Icon
|
||||
icon="mdi:alert-circle"
|
||||
style={{ fontSize: "48px", color: "#dc3545", marginBottom: "15px" }}
|
||||
/>
|
||||
<p style={{ color: "#dc3545", marginBottom: "10px", fontSize: "18px" }}>
|
||||
Не удалось улучшить текст
|
||||
</p>
|
||||
<p style={{ fontSize: "14px", color: "#666" }}>
|
||||
{errorMessage || "Произошла ошибка при улучшении текста. Проверьте настройки AI или попробуйте еще раз."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<h4 style={{ margin: "0 0 10px 0", fontSize: "16px", color: "#666" }}>
|
||||
Оригинальный текст:
|
||||
</h4>
|
||||
<div
|
||||
style={{
|
||||
padding: "15px",
|
||||
border: "1px solid var(--border-color)",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
maxHeight: "200px",
|
||||
overflowY: "auto",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.6",
|
||||
}}
|
||||
>
|
||||
{originalText || "(пусто)"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ margin: "0 0 10px 0", fontSize: "16px" }}>
|
||||
Улучшенный текст:
|
||||
</h4>
|
||||
<textarea
|
||||
value={localImprovedText}
|
||||
onChange={(e) => setLocalImprovedText(e.target.value)}
|
||||
style={{
|
||||
width: "100%",
|
||||
minHeight: "200px",
|
||||
maxHeight: "400px",
|
||||
padding: "15px",
|
||||
border: "1px solid var(--border-color)",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
color: "var(--text-primary)",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.6",
|
||||
fontFamily: "inherit",
|
||||
resize: "vertical",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
placeholder="Улучшенный текст появится здесь..."
|
||||
/>
|
||||
<p style={{ fontSize: "12px", color: "#666", marginTop: "8px" }}>
|
||||
Вы можете отредактировать улучшенный текст перед применением
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleApply}
|
||||
disabled={isLoading || hasError || !localImprovedText.trim()}
|
||||
>
|
||||
Применить
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={onClose} disabled={isLoading}>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -21,7 +21,6 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
||||
const [mergedContent, setMergedContent] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [deleteOriginalNotes, setDeleteOriginalNotes] = useState(false);
|
||||
const isClosedRef = useRef(false);
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
@ -35,7 +34,6 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
||||
setMergedContent("");
|
||||
setIsLoading(false);
|
||||
setIsSaving(false);
|
||||
setDeleteOriginalNotes(false);
|
||||
isClosedRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -46,7 +44,6 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
||||
setIsLoading(false);
|
||||
setIsSaving(false);
|
||||
setMergedContent("");
|
||||
setDeleteOriginalNotes(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
@ -98,27 +95,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
||||
time,
|
||||
});
|
||||
|
||||
// Удаляем исходные заметки, если тумблер включен
|
||||
if (deleteOriginalNotes) {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedNotes.map((note) => offlineNotesApi.delete(note.id))
|
||||
);
|
||||
showNotification(
|
||||
`Объединенная заметка сохранена! Удалено ${selectedNotes.length} исходных заметок.`,
|
||||
"success"
|
||||
);
|
||||
} catch (deleteError) {
|
||||
console.error("Ошибка удаления исходных заметок:", deleteError);
|
||||
showNotification(
|
||||
"Объединенная заметка сохранена, но произошла ошибка при удалении исходных заметок",
|
||||
"warning"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showNotification("Объединенная заметка сохранена!", "success");
|
||||
}
|
||||
|
||||
showNotification("Объединенная заметка сохранена!", "success");
|
||||
onSuccess();
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
@ -161,7 +138,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
||||
<div className="modal-body">
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: "center", padding: "40px 20px" }}>
|
||||
<div className="loading-spinner" style={{ margin: "0 auto 20px" }}></div>
|
||||
<div className="spinner" style={{ margin: "0 auto 20px" }}></div>
|
||||
<p>Объединяю заметки через ИИ...</p>
|
||||
<p style={{ fontSize: "14px", color: "#666", marginTop: "10px" }}>
|
||||
Выбрано заметок: {selectedNotes.length}
|
||||
@ -192,34 +169,6 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
||||
>
|
||||
<NotePreview content={mergedContent} />
|
||||
</div>
|
||||
<div
|
||||
className="form-group ai-toggle-group"
|
||||
style={{ marginTop: "20px", marginBottom: "10px" }}
|
||||
>
|
||||
<label className="ai-toggle-label">
|
||||
<div className="toggle-label-content">
|
||||
<span className="toggle-text-main">
|
||||
Удалить исходные заметки
|
||||
</span>
|
||||
<span className="toggle-text-desc">
|
||||
{deleteOriginalNotes
|
||||
? "Исходные заметки будут удалены после сохранения объединенной заметки"
|
||||
: "Исходные заметки останутся в списке после сохранения объединенной заметки"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="toggle-switch-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="delete-original-notes-toggle"
|
||||
className="toggle-checkbox"
|
||||
checked={deleteOriginalNotes}
|
||||
onChange={(e) => setDeleteOriginalNotes(e.target.checked)}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<span className="toggle-slider"></span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -9,9 +9,6 @@ import { useNotification } from "../../hooks/useNotification";
|
||||
import { offlineNotesApi } from "../../api/offlineNotesApi";
|
||||
import { aiApi } from "../../api/aiApi";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { GenerateTagsModal } from "./GenerateTagsModal";
|
||||
import { ImproveTextModal } from "./ImproveTextModal";
|
||||
import { extractTags } from "../../utils/markdown";
|
||||
|
||||
interface NoteEditorProps {
|
||||
onSave: () => void;
|
||||
@ -22,14 +19,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const [images, setImages] = useState<File[]>([]);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
const [showTagsModal, setShowTagsModal] = useState(false);
|
||||
const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
|
||||
const [isGeneratingTags, setIsGeneratingTags] = useState(false);
|
||||
const [tagsGenerationError, setTagsGenerationError] = useState(false);
|
||||
const [showImproveModal, setShowImproveModal] = useState(false);
|
||||
const [improvedText, setImprovedText] = useState("");
|
||||
const [improveError, setImproveError] = useState(false);
|
||||
const [improveErrorMessage, setImproveErrorMessage] = useState<string>("");
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
const [toolbarPosition, setToolbarPosition] = useState({ top: 0, left: 0 });
|
||||
const [hasSelection, setHasSelection] = useState(false);
|
||||
@ -94,85 +83,18 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
}
|
||||
|
||||
setIsAiLoading(true);
|
||||
setImproveError(false);
|
||||
setImproveErrorMessage("");
|
||||
setImprovedText("");
|
||||
setShowImproveModal(true);
|
||||
try {
|
||||
const improved = await aiApi.improveText(content);
|
||||
setImprovedText(improved);
|
||||
setImproveError(false);
|
||||
} catch (error: any) {
|
||||
const improvedText = await aiApi.improveText(content);
|
||||
setContent(improvedText);
|
||||
showNotification("Текст улучшен!", "success");
|
||||
} catch (error) {
|
||||
console.error("Ошибка улучшения текста:", error);
|
||||
setImproveError(true);
|
||||
setImproveErrorMessage(
|
||||
error.response?.data?.error || error.message || "Ошибка улучшения текста"
|
||||
);
|
||||
showNotification("Ошибка улучшения текста", "error");
|
||||
} finally {
|
||||
setIsAiLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyImprovedText = (text: string) => {
|
||||
setContent(text);
|
||||
showNotification("Текст улучшен!", "success");
|
||||
setShowImproveModal(false);
|
||||
setImprovedText("");
|
||||
setImproveError(false);
|
||||
setImproveErrorMessage("");
|
||||
};
|
||||
|
||||
const handleGenerateTags = async () => {
|
||||
if (!content.trim()) {
|
||||
showNotification("Введите текст для генерации тегов", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGeneratingTags(true);
|
||||
setTagsGenerationError(false);
|
||||
setSuggestedTags([]);
|
||||
setShowTagsModal(true);
|
||||
try {
|
||||
const tags = await aiApi.generateTags(content);
|
||||
if (tags && tags.length > 0) {
|
||||
setSuggestedTags(tags);
|
||||
setTagsGenerationError(false);
|
||||
} else {
|
||||
setTagsGenerationError(true);
|
||||
setShowTagsModal(false);
|
||||
showNotification("ИИ не смог предложить теги для этой заметки", "info");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Ошибка генерации тегов:", error);
|
||||
console.error("Детали ошибки:", error.response?.data);
|
||||
setTagsGenerationError(true);
|
||||
setShowTagsModal(false);
|
||||
const errorMessage = error.response?.data?.error || error.message || "Ошибка генерации тегов";
|
||||
showNotification(errorMessage, "error");
|
||||
} finally {
|
||||
setIsGeneratingTags(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectTags = (tags: string[]) => {
|
||||
if (tags.length === 0) return;
|
||||
|
||||
const existingTags = extractTags(content);
|
||||
const tagsToAdd = tags
|
||||
.filter((tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase()))
|
||||
.map((tag) => `#${tag}`)
|
||||
.join(" ");
|
||||
|
||||
if (tagsToAdd) {
|
||||
// Добавляем теги в конец заметки
|
||||
const newContent = content.trim() + (content.trim() ? "\n\n" : "") + tagsToAdd;
|
||||
setContent(newContent);
|
||||
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
||||
} else {
|
||||
showNotification("Все предлагаемые теги уже есть в заметке", "info");
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для определения активных форматов в выделенном тексте
|
||||
const getActiveFormats = useCallback(() => {
|
||||
const textarea = textareaRef.current;
|
||||
@ -1070,30 +992,15 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
<div className="save-button-container">
|
||||
<div className="action-buttons">
|
||||
{aiEnabled && (
|
||||
<>
|
||||
<button
|
||||
className="btnSave btnAI"
|
||||
onClick={handleAiImprove}
|
||||
disabled={isAiLoading}
|
||||
title="Улучшить или создать текст через ИИ"
|
||||
>
|
||||
<Icon icon="mdi:robot" />
|
||||
<span className="btnAI-text">
|
||||
{isAiLoading ? "Обработка..." : "Помощь ИИ"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="btnSave btnAI"
|
||||
onClick={handleGenerateTags}
|
||||
disabled={isGeneratingTags || isAiLoading}
|
||||
title="Сгенерировать теги через ИИ"
|
||||
>
|
||||
<Icon icon="mdi:tag-multiple" />
|
||||
<span className="btnAI-text">
|
||||
{isGeneratingTags ? "Генерация..." : "Теги ИИ"}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
className="btnSave btnAI"
|
||||
onClick={handleAiImprove}
|
||||
disabled={isAiLoading}
|
||||
title="Улучшить или создать текст через ИИ"
|
||||
>
|
||||
<Icon icon="mdi:robot" />
|
||||
{isAiLoading ? "Обработка..." : "Помощь ИИ"}
|
||||
</button>
|
||||
)}
|
||||
<button className="btnSave" onClick={handleSave}>
|
||||
Сохранить
|
||||
@ -1101,36 +1008,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
</div>
|
||||
<span className="save-hint">или нажмите Alt + Enter</span>
|
||||
</div>
|
||||
|
||||
<GenerateTagsModal
|
||||
isOpen={showTagsModal}
|
||||
onClose={() => {
|
||||
setShowTagsModal(false);
|
||||
setSuggestedTags([]);
|
||||
setTagsGenerationError(false);
|
||||
}}
|
||||
onSelectTags={handleSelectTags}
|
||||
suggestedTags={suggestedTags}
|
||||
existingTags={extractTags(content)}
|
||||
isLoading={isGeneratingTags}
|
||||
hasError={tagsGenerationError}
|
||||
/>
|
||||
|
||||
<ImproveTextModal
|
||||
isOpen={showImproveModal}
|
||||
onClose={() => {
|
||||
setShowImproveModal(false);
|
||||
setImprovedText("");
|
||||
setImproveError(false);
|
||||
setImproveErrorMessage("");
|
||||
}}
|
||||
onApply={handleApplyImprovedText}
|
||||
originalText={content}
|
||||
improvedText={improvedText}
|
||||
isLoading={isAiLoading}
|
||||
hasError={improveError}
|
||||
errorMessage={improveErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -20,8 +20,6 @@ import { NotePreview } from "./NotePreview";
|
||||
import { ImageUpload } from "./ImageUpload";
|
||||
import { FileUpload } from "./FileUpload";
|
||||
import { aiApi } from "../../api/aiApi";
|
||||
import { GenerateTagsModal } from "./GenerateTagsModal";
|
||||
import { extractTags } from "../../utils/markdown";
|
||||
|
||||
interface NoteItemProps {
|
||||
note: Note;
|
||||
@ -61,10 +59,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const [localPreviewMode, setLocalPreviewMode] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isLongNote, setIsLongNote] = useState(false);
|
||||
const [showTagsModal, setShowTagsModal] = useState(false);
|
||||
const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
|
||||
const [isGeneratingTags, setIsGeneratingTags] = useState(false);
|
||||
const [tagsGenerationError, setTagsGenerationError] = useState(false);
|
||||
const editTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@ -191,57 +185,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateTags = async () => {
|
||||
if (!editContent.trim()) {
|
||||
showNotification("Введите текст для генерации тегов", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGeneratingTags(true);
|
||||
setTagsGenerationError(false);
|
||||
setSuggestedTags([]);
|
||||
setShowTagsModal(true);
|
||||
try {
|
||||
const tags = await aiApi.generateTags(editContent);
|
||||
if (tags && tags.length > 0) {
|
||||
setSuggestedTags(tags);
|
||||
setTagsGenerationError(false);
|
||||
} else {
|
||||
setTagsGenerationError(true);
|
||||
setShowTagsModal(false);
|
||||
showNotification("ИИ не смог предложить теги для этой заметки", "info");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Ошибка генерации тегов:", error);
|
||||
console.error("Детали ошибки:", error.response?.data);
|
||||
setTagsGenerationError(true);
|
||||
setShowTagsModal(false);
|
||||
const errorMessage = error.response?.data?.error || error.message || "Ошибка генерации тегов";
|
||||
showNotification(errorMessage, "error");
|
||||
} finally {
|
||||
setIsGeneratingTags(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectTags = (tags: string[]) => {
|
||||
if (tags.length === 0) return;
|
||||
|
||||
const existingTags = extractTags(editContent);
|
||||
const tagsToAdd = tags
|
||||
.filter((tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase()))
|
||||
.map((tag) => `#${tag}`)
|
||||
.join(" ");
|
||||
|
||||
if (tagsToAdd) {
|
||||
// Добавляем теги в конец заметки
|
||||
const newContent = editContent.trim() + (editContent.trim() ? "\n\n" : "") + tagsToAdd;
|
||||
setEditContent(newContent);
|
||||
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
||||
} else {
|
||||
showNotification("Все предлагаемые теги уже есть в заметке", "info");
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для определения активных форматов в выделенном тексте
|
||||
const getActiveFormats = useCallback(() => {
|
||||
const textarea = editTextareaRef.current;
|
||||
@ -867,6 +810,10 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleArchiveClick = () => {
|
||||
setShowArchiveModal(true);
|
||||
};
|
||||
|
||||
// Авторасширение textarea в режиме редактирования
|
||||
useEffect(() => {
|
||||
if (!isEditing) return;
|
||||
@ -1189,6 +1136,19 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
>
|
||||
<div className="date">
|
||||
<span className="date-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => onSelect && onSelect(note.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
cursor: "pointer",
|
||||
marginRight: "10px",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
/>
|
||||
{formatDate()}
|
||||
{note.is_pinned ? (
|
||||
<span className="pin-indicator">
|
||||
@ -1222,19 +1182,13 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
>
|
||||
<Icon icon="mdi:pencil" />
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => onSelect && onSelect(note.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{/* <div
|
||||
<div
|
||||
className="notesHeaderBtn"
|
||||
onClick={handleArchiveClick}
|
||||
title="В архив"
|
||||
>
|
||||
<Icon icon="mdi:delete" />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1478,30 +1432,15 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
<div className="save-button-container">
|
||||
<div className="action-buttons">
|
||||
{aiEnabled && (
|
||||
<>
|
||||
<button
|
||||
className="btnSave btnAI"
|
||||
onClick={handleAiImprove}
|
||||
disabled={isAiLoading}
|
||||
title="Улучшить или создать текст через ИИ"
|
||||
>
|
||||
<Icon icon="mdi:robot" />
|
||||
<span className="btnAI-text">
|
||||
{isAiLoading ? "Обработка..." : "Помощь ИИ"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="btnSave btnAI"
|
||||
onClick={handleGenerateTags}
|
||||
disabled={isGeneratingTags || isAiLoading}
|
||||
title="Сгенерировать теги через ИИ"
|
||||
>
|
||||
<Icon icon="mdi:tag-multiple" />
|
||||
<span className="btnAI-text">
|
||||
{isGeneratingTags ? "Генерация..." : "Теги ИИ"}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
className="btnSave btnAI"
|
||||
onClick={handleAiImprove}
|
||||
disabled={isAiLoading}
|
||||
title="Улучшить или создать текст через ИИ"
|
||||
>
|
||||
<Icon icon="mdi:robot" />
|
||||
{isAiLoading ? "Обработка..." : "Помощь ИИ"}
|
||||
</button>
|
||||
)}
|
||||
<button className="btnSave" onClick={handleSaveEdit}>
|
||||
Сохранить
|
||||
@ -1613,20 +1552,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
confirmText="Архивировать"
|
||||
cancelText="Отмена"
|
||||
/>
|
||||
|
||||
<GenerateTagsModal
|
||||
isOpen={showTagsModal}
|
||||
onClose={() => {
|
||||
setShowTagsModal(false);
|
||||
setSuggestedTags([]);
|
||||
setTagsGenerationError(false);
|
||||
}}
|
||||
onSelectTags={handleSelectTags}
|
||||
suggestedTags={suggestedTags}
|
||||
existingTags={extractTags(editContent)}
|
||||
isLoading={isGeneratingTags}
|
||||
hasError={tagsGenerationError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -33,7 +33,6 @@ const NotesPage: React.FC = () => {
|
||||
const selectedDate = useAppSelector((state) => state.notes.selectedDate);
|
||||
const selectedTag = useAppSelector((state) => state.notes.selectedTag);
|
||||
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
|
||||
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
|
||||
|
||||
const hasFilters = !!(selectedDate || selectedTag || searchQuery);
|
||||
|
||||
@ -116,13 +115,13 @@ const NotesPage: React.FC = () => {
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
// Архивируем все выбранные заметки
|
||||
// Удаляем все выбранные заметки
|
||||
await Promise.all(
|
||||
selectedNoteIds.map((id) => offlineNotesApi.archive(id))
|
||||
selectedNoteIds.map((id) => offlineNotesApi.delete(id))
|
||||
);
|
||||
|
||||
showNotification(
|
||||
`Архивировано заметок: ${selectedNoteIds.length}`,
|
||||
`Удалено заметок: ${selectedNoteIds.length}`,
|
||||
"success"
|
||||
);
|
||||
setSelectedNoteIds([]);
|
||||
@ -132,8 +131,8 @@ const NotesPage: React.FC = () => {
|
||||
notesListRef.current.reloadNotes();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Ошибка архивирования заметок:", error);
|
||||
showNotification("Ошибка архивирования заметок", "error");
|
||||
console.error("Ошибка удаления заметок:", error);
|
||||
showNotification("Ошибка удаления заметок", "error");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
@ -194,7 +193,7 @@ const NotesPage: React.FC = () => {
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
{selectedNoteIds.length >= 2 && aiEnabled && (
|
||||
{selectedNoteIds.length >= 2 && (
|
||||
<button
|
||||
onClick={handleMergeNotes}
|
||||
style={{
|
||||
@ -242,15 +241,15 @@ const NotesPage: React.FC = () => {
|
||||
width: "56px",
|
||||
height: "56px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme === "dark" ? "#FF9800" : "#FF9800",
|
||||
backgroundColor: theme === "dark" ? "#F44336" : "#E53935",
|
||||
color: "white",
|
||||
border: "none",
|
||||
cursor: isDeleting ? "not-allowed" : "pointer",
|
||||
opacity: isDeleting ? 0.6 : 1,
|
||||
boxShadow:
|
||||
theme === "dark"
|
||||
? "0 4px 12px rgba(255, 152, 0, 0.4)"
|
||||
: "0 4px 12px rgba(255, 152, 0, 0.4)",
|
||||
? "0 4px 12px rgba(244, 67, 54, 0.4)"
|
||||
: "0 4px 12px rgba(229, 57, 53, 0.4)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@ -262,20 +261,20 @@ const NotesPage: React.FC = () => {
|
||||
e.currentTarget.style.transform = "scale(1.1)";
|
||||
e.currentTarget.style.boxShadow =
|
||||
theme === "dark"
|
||||
? "0 6px 16px rgba(255, 152, 0, 0.6)"
|
||||
: "0 6px 16px rgba(255, 152, 0, 0.6)";
|
||||
? "0 6px 16px rgba(244, 67, 54, 0.6)"
|
||||
: "0 6px 16px rgba(229, 57, 53, 0.6)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1)";
|
||||
e.currentTarget.style.boxShadow =
|
||||
theme === "dark"
|
||||
? "0 4px 12px rgba(255, 152, 0, 0.4)"
|
||||
: "0 4px 12px rgba(255, 152, 0, 0.4)";
|
||||
? "0 4px 12px rgba(244, 67, 54, 0.4)"
|
||||
: "0 4px 12px rgba(229, 57, 53, 0.4)";
|
||||
}}
|
||||
title={`Архивировать ${selectedNoteIds.length} ${selectedNoteIds.length === 1 ? "заметку" : selectedNoteIds.length > 4 ? "заметок" : "заметки"}`}
|
||||
title={`Удалить ${selectedNoteIds.length} ${selectedNoteIds.length === 1 ? "заметку" : selectedNoteIds.length > 4 ? "заметок" : "заметки"}`}
|
||||
>
|
||||
<Icon icon="mdi:archive" />
|
||||
<Icon icon="mdi:delete" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@ -290,22 +289,22 @@ const NotesPage: React.FC = () => {
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
title="Архивирование заметок"
|
||||
title="Удаление заметок"
|
||||
message={
|
||||
<p>
|
||||
Вы уверены, что хотите архивировать{" "}
|
||||
Вы уверены, что хотите удалить{" "}
|
||||
<strong>{selectedNoteIds.length}</strong>{" "}
|
||||
{selectedNoteIds.length === 1
|
||||
? "заметку"
|
||||
: selectedNoteIds.length > 4
|
||||
? "заметок"
|
||||
: "заметки"}
|
||||
? Заметки можно будет восстановить из архива в настройках.
|
||||
? Это действие нельзя отменить.
|
||||
</p>
|
||||
}
|
||||
confirmText={isDeleting ? "Архивирование..." : "Архивировать"}
|
||||
confirmText={isDeleting ? "Удаление..." : "Удалить"}
|
||||
cancelText="Отмена"
|
||||
confirmType="primary"
|
||||
confirmType="danger"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -245,7 +245,7 @@ const SettingsPage: React.FC = () => {
|
||||
setAiEnabled(checked);
|
||||
localStorage.setItem("ai_enabled", checked ? "1" : "0");
|
||||
showNotification(
|
||||
checked ? "Функции ИИ включены" : "Функции ИИ отключены",
|
||||
checked ? "Помощь ИИ включена" : "Помощь ИИ отключена",
|
||||
"success"
|
||||
);
|
||||
} catch (error: any) {
|
||||
@ -726,16 +726,11 @@ const SettingsPage: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
<div className="toggle-label-content">
|
||||
<span className="toggle-text-main">Включить функции ИИ</span>
|
||||
<span className="toggle-text-main">Включить помощь ИИ</span>
|
||||
<span className="toggle-text-desc">
|
||||
{checkAiSettingsFilled() ? (
|
||||
<ul style={{ margin: "8px 0 0 20px", padding: 0 }}>
|
||||
<li>Улучшение текста заметок</li>
|
||||
<li>Объединение заметок</li>
|
||||
</ul>
|
||||
) : (
|
||||
"Сначала заполните API Key, Base URL и Модель ниже"
|
||||
)}
|
||||
{checkAiSettingsFilled()
|
||||
? 'Показывать кнопку "Помощь ИИ" в редакторах заметок'
|
||||
: "Сначала заполните API Key, Base URL и Модель ниже"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="toggle-switch-wrapper">
|
||||
|
||||
@ -1072,9 +1072,7 @@ textarea:focus {
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btnAI {
|
||||
@ -1082,13 +1080,7 @@ textarea:focus {
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
min-height: 44px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btnAI:hover {
|
||||
@ -1111,7 +1103,7 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.btnSave {
|
||||
padding: 8px 16px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-width: 1px;
|
||||
background: var(--bg-secondary);
|
||||
@ -1120,13 +1112,7 @@ textarea:focus {
|
||||
border-radius: 5px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
min-height: 44px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btnSave:hover {
|
||||
@ -1168,58 +1154,6 @@ textarea:focus {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.note-actions input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid var(--border-primary);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-secondary);
|
||||
transition: all 0.2s ease;
|
||||
accent-color: var(--accent-color);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-actions input[type="checkbox"]:hover {
|
||||
border-color: var(--accent-color);
|
||||
background-color: var(--bg-quaternary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.1);
|
||||
}
|
||||
|
||||
.note-actions input[type="checkbox"]:checked {
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.note-actions input[type="checkbox"]:checked::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -60%) rotate(45deg);
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.note-actions input[type="checkbox"]:checked:hover {
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.2);
|
||||
}
|
||||
|
||||
.note-actions input[type="checkbox"]:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.15);
|
||||
}
|
||||
|
||||
.notesHeaderBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -1581,8 +1515,6 @@ textarea:focus {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -1596,8 +1528,6 @@ textarea:focus {
|
||||
@media (max-width: 768px) {
|
||||
.markdown-buttons.markdown-buttons--edit .btnMarkdown {
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
}
|
||||
@ -1618,8 +1548,6 @@ textarea:focus {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
min-height: 44px; /* Минимальная высота для touch */
|
||||
min-width: 44px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -1740,9 +1668,8 @@ textarea:focus {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
flex-shrink: 0;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.floating-toolbar-btn:hover {
|
||||
@ -3039,8 +2966,8 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.markdown-buttons .btnMarkdown {
|
||||
flex: 0 0 auto;
|
||||
min-width: 44px;
|
||||
flex: 0 1 auto;
|
||||
min-width: auto;
|
||||
margin-right: 0;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
@ -3945,8 +3872,6 @@ textarea:focus {
|
||||
/* Улучшения для мобильных устройств */
|
||||
.markdown-buttons .btnMarkdown {
|
||||
min-height: 48px; /* Увеличиваем высоту для touch */
|
||||
min-width: 48px;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
margin: 2px;
|
||||
}
|
||||
@ -4264,17 +4189,11 @@ textarea:focus {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
padding: 8px 16px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 44px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
@ -5074,8 +4993,6 @@ textarea:focus {
|
||||
/* Markdown кнопки */
|
||||
.markdown-buttons .btnMarkdown {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 10px;
|
||||
margin: 2px;
|
||||
font-size: 13px;
|
||||
@ -5125,8 +5042,6 @@ textarea:focus {
|
||||
}
|
||||
|
||||
.markdown-buttons .btnMarkdown {
|
||||
min-width: 40px;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user