Добавлен field_label в результат переименования файлов, исправлена загрузка черновиков, обновлен формат пути S3 с project_name

This commit is contained in:
Fedor
2025-11-22 09:38:38 +03:00
parent d3ba054027
commit 486f3619ff
212 changed files with 6704 additions and 123 deletions

View File

@@ -1,10 +1,65 @@
{ {
"mcpServers": { "mcpServers": {
"context7": { "context7": {
"url": "https://mcp.context7.com/mcp", "url": "https://mcp.context7.com/mcp",
"headers": { "headers": {
"CONTEXT7_API_KEY": "ctx7sk-541e7992-c38f-442f-8902-ae99645f2477" "CONTEXT7_API_KEY": "ctx7sk-541e7992-c38f-442f-8902-ae99645f2477"
}
} }
} },
"shadcn": {
"command": "/usr/bin/docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-v", "/var/www/fastuser/data/www/crm.clientright.ru:/workspace",
"-w", "/workspace",
"node:20-alpine",
"npx",
"-y",
"shadcn@latest",
"mcp"
],
"env": {}
},
"antd-components": {
"command": "/usr/bin/docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-v", "/var/www/fastuser/data/www/crm.clientright.ru:/workspace",
"-w", "/workspace",
"node:20-alpine",
"npx",
"-y",
"@jzone-mcp/antd-components-mcp"
],
"env": {}
},
"n8n-mcp": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-e", "MCP_MODE=stdio",
"-e", "LOG_LEVEL=error",
"-e", "DISABLE_CONSOLE_OUTPUT=true",
"-e", "N8N_API_URL=https://n8n.clientright.pro/",
"-e", "N8N_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5MzMwYWVjZC1hYjExLTQxODEtOWIyYy1iMDZhZWEzMTNmNzQiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzUzNjg3NDM4fQ.XJjyYXXOoO4eUGAfkSVRMJzLYvi25hczsp2F7j4UV7Y",
"ghcr.io/czlonkowski/n8n-mcp:latest"
]
},
"memory": {
"url": "http://185.197.75.249:9000/sse"
}
} }
}

View File

@@ -211,3 +211,9 @@ composer require phpoffice/phpword
Все компоненты реализованы, протестированы и готовы к использованию в n8n workflow. Все компоненты реализованы, протестированы и готовы к использованию в n8n workflow.

View File

@@ -432,3 +432,9 @@ PUBLISH ai:response:task-xxx {"success": true, "documentUrl": "..."}
- Ошибки изолированы - Ошибки изолированы
- Легко отлаживать - Легко отлаживать

View File

@@ -197,3 +197,9 @@ https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_
- Документ сразу доступен для редактирования в OnlyOffice - Документ сразу доступен для редактирования в OnlyOffice
- Путь формируется автоматически: `{module}/{recordName}_{recordId}/{fileName}.{ext}` - Путь формируется автоматически: `{module}/{recordName}_{recordId}/{fileName}.{ext}`

View File

@@ -244,3 +244,9 @@ _курсив_
3. **Гибкость** — можно комбинировать элементы 3. **Гибкость** — можно комбинировать элементы
4. **Автоматическое форматирование** — документ получается красивым без ручной правки 4. **Автоматическое форматирование** — документ получается красивым без ручной правки

View File

@@ -152,3 +152,9 @@ curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_
}' }'
``` ```

View File

@@ -211,3 +211,9 @@ curl -u admin:office "https://office.clientright.ru:8443/remote.php/dav/files/ad
- [Nextcloud WebDAV API](https://docs.nextcloud.com/server/latest/user_manual/files/webdav.html) - [Nextcloud WebDAV API](https://docs.nextcloud.com/server/latest/user_manual/files/webdav.html)
- [OnlyOffice Integration](https://api.onlyoffice.com/) - [OnlyOffice Integration](https://api.onlyoffice.com/)

View File

@@ -246,3 +246,9 @@ echo json_encode(['success' => true, 'templates' => $templates]);
3. **Решение:** Использовать WebDAV PROPFIND для получения списка файлов из папки Templates 3. **Решение:** Использовать WebDAV PROPFIND для получения списка файлов из папки Templates
4. **Статус:** Наш текущий подход (WebDAV + PHPWord) является правильным и оптимальным решением 4. **Статус:** Наш текущий подход (WebDAV + PHPWord) является правильным и оптимальным решением

View File

@@ -133,3 +133,9 @@ curl "https://office.clientright.ru:8443/index.php/apps/onlyoffice/ajax/template
3. Протестировать получение списка через `list_templates.php` 3. Протестировать получение списка через `list_templates.php`
4. Использовать шаблоны через `create_from_template.php` 4. Использовать шаблоны через `create_from_template.php`

View File

@@ -115,10 +115,34 @@ try {
continue; // Пропускаем пустые сообщения continue; // Пропускаем пустые сообщения
} }
// Формируем timestamp в ISO формате для JavaScript
$timestamp = null;
if (isset($item['created_at']) && !empty($item['created_at'])) {
$createdAt = $item['created_at'];
// Если created_at уже в ISO формате (содержит 'T'), используем как есть
if (strpos($createdAt, 'T') !== false) {
// Уже в ISO формате (например, "2025-11-14T06:21:55.207Z"), используем как есть
$timestamp = $createdAt;
} else {
// Если в другом формате, преобразуем в ISO
$parsedTime = strtotime($createdAt);
if ($parsedTime !== false) {
$timestamp = date('c', $parsedTime); // ISO 8601 формат
} else {
// Если не удалось распарсить, используем текущее время
error_log("Chat History: Failed to parse created_at: {$createdAt}, using current time");
$timestamp = date('c');
}
}
} else {
// Если нет created_at, используем текущее время в ISO формате
$timestamp = date('c'); // ISO 8601 формат
}
$message = [ $message = [
'type' => isset($item['sender_type']) && $item['sender_type'] === 'user' ? 'user' : 'assistant', 'type' => isset($item['sender_type']) && $item['sender_type'] === 'user' ? 'user' : 'assistant',
'message' => $item['content'] ?? '', 'message' => $item['content'] ?? '',
'timestamp' => isset($item['created_at']) ? date('H:i:s', strtotime($item['created_at'])) : date('H:i:s'), 'timestamp' => $timestamp,
'id' => $item['id'] ?? '', 'id' => $item['id'] ?? '',
'dialog_id' => $item['dialog_id'] ?? '' 'dialog_id' => $item['dialog_id'] ?? ''
]; ];
@@ -131,7 +155,7 @@ try {
$history[] = [ $history[] = [
'type' => 'assistant', 'type' => 'assistant',
'message' => "Привет! Я ваш AI ассистент. Работаем с '{$projectName}'. Чем могу помочь?", 'message' => "Привет! Я ваш AI ассистент. Работаем с '{$projectName}'. Чем могу помочь?",
'timestamp' => date('H:i:s'), 'timestamp' => date('c'), // ISO 8601 формат
'id' => 'welcome-' . time(), 'id' => 'welcome-' . time(),
'dialog_id' => 'new-dialog' 'dialog_id' => 'new-dialog'
]; ];

View File

@@ -0,0 +1,175 @@
<?php
/*********************************************************************************
* API-интерфейс для создания Проекта КлиентПрав из web-формы ticket_form
* Уникальность проекта обеспечивается по claim_id (cf_2620)
* Автор: GPT-5.1 Codex & Фёдор, 2025-11-15
********************************************************************************/
include_once 'include/Webservices/Query.php';
include_once 'modules/Users/Users.php';
require_once('include/Webservices/Utils.php');
require_once 'include/Webservices/Create.php';
require_once 'include/Webservices/Retrieve.php';
require_once 'includes/Loader.php';
vimport ('includes.runtime.Globals');
vimport ('includes.runtime.BaseModel');
vimport ('includes.runtime.LanguageHandler');
/**
* Создание проекта КлиентПрав по заявке ticket_form
*
* @param string $contact_id - ID контакта (обязательное)
* @param string $claim_id - Уникальный ID обращения (обязательное, cf_2620)
* @param string $session_id - Сессия фронтенда (опционально, cf_2618)
* @param string $description - Описание пользователя (опционально, description)
* @param string $ai_response - Ответ AI модели (опционально, cf_2622)
* @param string $phone - Телефон для генерации имени (опционально)
* @param string $firstname - Имя контакта (опционально)
* @param string $lastname - Фамилия контакта (опционально)
* @return array {project_id, project_name, is_new}
*/
function vtws_createclientproject($contact_id, $claim_id, $session_id = '', $description = '', $ai_response = '', $phone = '', $firstname = '', $lastname = '', $user = false) {
ob_start();
$logPrefix = date("Y-m-d H:i:s") . ' ';
file_put_contents('logs/CreateClientProject.log', $logPrefix . json_encode($_REQUEST) . PHP_EOL, FILE_APPEND);
try {
global $adb, $current_user;
if (empty($claim_id)) {
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан claim_id");
}
if (empty($contact_id)) {
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан ID контакта");
}
$claim_id = trim($claim_id);
$session_id = trim($session_id);
// Нормализуем contact_id
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
$contactIdWithPrefix = '12x' . $contactIdNumeric;
// Подтягиваем данные контакта (фамилию/телефон), если не переданы
if (empty($lastname) || empty($firstname) || empty($phone)) {
try {
$contactRecord = vtws_retrieve($contactIdWithPrefix, $current_user);
if (empty($lastname) && !empty($contactRecord['lastname'])) {
$lastname = $contactRecord['lastname'];
}
if (empty($firstname) && !empty($contactRecord['firstname'])) {
$firstname = $contactRecord['firstname'];
}
if (empty($phone) && !empty($contactRecord['phone'])) {
$phone = preg_replace('/[^0-9]/', '', $contactRecord['phone']);
}
} catch (Exception $e) {
file_put_contents('logs/CreateClientProject.log', $logPrefix . '⚠️ Не удалось получить контакт: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
}
}
$isNew = false;
$output = null;
file_put_contents('logs/CreateClientProject.log', $logPrefix . "🔎 Ищем проект по claim_id={$claim_id}" . PHP_EOL, FILE_APPEND);
// Ищем проект по claim_id (cf_2620)
$query = "SELECT p.projectid
FROM vtiger_project p
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
INNER JOIN vtiger_crmentity e ON e.crmid = p.projectid
WHERE e.deleted = 0
AND pcf.cf_2620 = ?
LIMIT 1";
$result = $adb->pquery($query, array($claim_id));
if (!$result) {
throw new Exception("SQL error while searching project");
}
$projectName = '';
if ($adb->num_rows($result) > 0) {
$output = $adb->query_result($result, 0, 'projectid');
$isNew = false;
file_put_contents('logs/CreateClientProject.log', $logPrefix . "✅ Проект найден по claim_id {$claim_id}: {$output}" . PHP_EOL, FILE_APPEND);
} else {
// Генерируем имя проекта
$lastname = trim($lastname);
if (!empty($lastname)) {
$projectName = $lastname . '_КлиентПрав';
} elseif (!empty($phone)) {
$projectName = $phone . '_КлиентПрав';
} else {
$projectName = 'КлиентПрав_' . $claim_id;
}
$params = array(
'projectname' => $projectName,
'projectstatus' => 'Черновик',
'projecttype' => 'претензионно-исковая работа',
'linktoaccountscontacts' => $contactIdWithPrefix,
'cf_1994' => '11x62345', // Заявитель (МОО КлиентПрав)
'cf_2620' => $claim_id,
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id)
);
if (!empty($session_id)) {
$params['cf_2618'] = $session_id;
}
if (!empty($description)) {
$params['description'] = $description;
}
if (!empty($ai_response)) {
$params['cf_2622'] = $ai_response;
}
file_put_contents('logs/CreateClientProject.log', $logPrefix . 'Массив для создания: ' . json_encode($params) . PHP_EOL, FILE_APPEND);
try {
$project = vtws_create('Project', $params, $current_user);
$output = substr($project['id'], 3);
$isNew = true;
file_put_contents('logs/CreateClientProject.log', $logPrefix . "✅ Создан новый проект: {$output}" . PHP_EOL, FILE_APPEND);
} catch (WebServiceException $ex) {
file_put_contents('logs/CreateClientProject.log', $logPrefix . '❌ Ошибка создания: ' . $ex->getMessage() . PHP_EOL, FILE_APPEND);
throw $ex;
}
}
// Получаем название проекта (если проект был найден, а не создан)
if (empty($projectName) && !empty($output)) {
try {
$query = "SELECT projectname FROM vtiger_project WHERE projectid = ? LIMIT 1";
$result = $adb->pquery($query, array($output));
if ($adb->num_rows($result) > 0) {
$projectName = $adb->query_result($result, 0, 'projectname');
file_put_contents('logs/CreateClientProject.log', $logPrefix . "📝 Получено название проекта: {$projectName}" . PHP_EOL, FILE_APPEND);
}
} catch (Exception $e) {
file_put_contents('logs/CreateClientProject.log', $logPrefix . '⚠️ Не удалось получить название проекта: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
}
}
$result = array(
'project_id' => $output,
'project_name' => $projectName,
'is_new' => $isNew
);
file_put_contents('logs/CreateClientProject.log', $logPrefix . 'Return: ' . json_encode($result) . PHP_EOL, FILE_APPEND);
ob_end_clean();
return $result;
} catch (Exception $ex) {
file_put_contents('logs/CreateClientProject.log', $logPrefix . '❌ Exception: ' . $ex->getMessage() . PHP_EOL, FILE_APPEND);
ob_end_clean();
throw $ex;
}
}

View File

@@ -65,6 +65,16 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
global $adb, $current_user; global $adb, $current_user;
// Нормализуем ID контакта и проекта (можно передавать как "12x123" или "123")
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
$projectIdNumeric = preg_replace('/[^0-9]/', '', $project_id);
$contactWsId = '12x' . $contactIdNumeric;
$projectWsId = '33x' . $projectIdNumeric;
$logstring = date('Y-m-d H:i:s').' Нормализовали ID: contact='.$contactIdNumeric.' (raw='.$contact_id.'), project='.$projectIdNumeric.' (raw='.$project_id.')'.PHP_EOL;
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
// Маппинг типов событий на русские названия для категории // Маппинг типов событий на русские названия для категории
$eventTypeMap = array( $eventTypeMap = array(
'delay_flight' => 'Задержка рейса', 'delay_flight' => 'Задержка рейса',
@@ -108,8 +118,8 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
'parent_id' => '11x67458', // Заявитель - контрагент 'parent_id' => '11x67458', // Заявитель - контрагент
'ticketcategories' => $ticketCategory, 'ticketcategories' => $ticketCategory,
'ticketstatus' => 'рассмотрение', 'ticketstatus' => 'рассмотрение',
'contact_id' => '12x'.$contact_id, 'contact_id' => $contactWsId,
'cf_2066' => '33x'.$project_id, // Связь с проектом 'cf_2066' => $projectWsId, // Связь с проектом
'ticketpriorities' => 'High', 'ticketpriorities' => 'High',
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id), 'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id),
'description' => $fullDescription 'description' => $fullDescription
@@ -126,6 +136,32 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
$logstring = date('Y-m-d H:i:s').' ✅ Создана Заявка id='.$ticketId.' ticket_no='.$ticketNumber.PHP_EOL; $logstring = date('Y-m-d H:i:s').' ✅ Создана Заявка id='.$ticketId.' ticket_no='.$ticketNumber.PHP_EOL;
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND); file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
// 🚧 Создаём двустороннюю связь между Проектом и Заявкой
try {
$relationCheck = $adb->pquery(
"SELECT 1 FROM vtiger_crmentityrel
WHERE (crmid = ? AND relcrmid = ?)
OR (crmid = ? AND relcrmid = ?)
LIMIT 1",
array($projectIdNumeric, $ticketId, $ticketId, $projectIdNumeric)
);
if (!$relationCheck || $adb->num_rows($relationCheck) === 0) {
$adb->pquery(
"INSERT INTO vtiger_crmentityrel (crmid, module, relcrmid, relmodule) VALUES (?, ?, ?, ?)",
array($projectIdNumeric, 'Project', $ticketId, 'HelpDesk')
);
$logstring = date('Y-m-d H:i:s').' 🔗 Добавлена связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.')'.PHP_EOL;
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
} else {
$logstring = date('Y-m-d H:i:s').' 🔗 Связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.') уже существует'.PHP_EOL;
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
}
} catch (Exception $relEx) {
$logstring = date('Y-m-d H:i:s').' ⚠️ Ошибка связывания Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.'): '.$relEx->getMessage().PHP_EOL;
file_put_contents('logs/CreateWebClaim.log', $logstring, FILE_APPEND);
}
// Возвращаем массив (vTiger сам сделает json_encode) // Возвращаем массив (vTiger сам сделает json_encode)
$output = array( $output = array(
'ticket_id' => $ticketId, 'ticket_id' => $ticketId,

View File

@@ -52,27 +52,28 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
// Валидация: убираем пробелы из номера полиса // Валидация: убираем пробелы из номера полиса
$policy_number = trim($policy_number); $policy_number = trim($policy_number);
$logstring = date('Y-m-d H:i:s').' Ищем проект по policy_number='.$policy_number.' И contact_id='.$contact_id.PHP_EOL; // Нормализуем contact_id: допускаем как "12x12345", так и "12345"
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
$contactIdWithPrefix = '12x' . $contactIdNumeric;
$logstring = date('Y-m-d H:i:s').' Ищем проект по policy_number='.$policy_number.' И contact_id='.$contactIdNumeric.' (raw='.$contact_id.')'.PHP_EOL;
file_put_contents('logs/CreateWebProject.log', $logstring, FILE_APPEND); file_put_contents('logs/CreateWebProject.log', $logstring, FILE_APPEND);
global $adb, $current_user; global $adb, $current_user;
$isNew = false; // Флаг: создан ли проект сейчас $isNew = false; // Флаг: создан ли проект сейчас
// Проверяем существование проекта по номеру полиса И привязке к контакту // Проверяем существование проекта по номеру полиса И прямой привязке к контакту
// (т.к. по одному полису может быть несколько застрахованных лиц) // (без зависимости от заполнения vtiger_crmentityrel)
$query = "SELECT p.projectid $query = "SELECT p.projectid
FROM vtiger_project p FROM vtiger_project p
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid INNER JOIN vtiger_crmentity e ON e.crmid = p.projectid
LEFT JOIN vtiger_crmentityrel rel ON
(rel.crmid = p.projectid AND rel.relcrmid = ?)
OR (rel.relcrmid = p.projectid AND rel.crmid = ?)
WHERE e.deleted = 0 WHERE e.deleted = 0
AND pcf.cf_1885 = ? AND pcf.cf_1885 = ?
AND rel.crmid IS NOT NULL AND p.linktoaccountscontacts = ?
LIMIT 1"; LIMIT 1";
$result = $adb->pquery($query, array($contact_id, $contact_id, $policy_number)); $result = $adb->pquery($query, array($policy_number, $contactIdNumeric));
if ($adb->num_rows($result) > 0) { if ($adb->num_rows($result) > 0) {
// Проект существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!) // Проект существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!)
@@ -90,7 +91,7 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
'projectname' => $projectname, 'projectname' => $projectname,
'projectstatus' => 'модерация', 'projectstatus' => 'модерация',
'projecttype' => 'ерв урегулирование', 'projecttype' => 'ерв урегулирование',
'linktoaccountscontacts' => '12x'.$contact_id, // Привязка к контакту 'linktoaccountscontacts' => $contactIdWithPrefix, // Привязка к контакту
'cf_1994' => '11x67458', // Заявитель (контрагент record=67458) 'cf_1994' => '11x67458', // Заявитель (контрагент record=67458)
'cf_1885' => $policy_number, // Номер полиса 'cf_1885' => $policy_number, // Номер полиса
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id) 'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id)

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -53,4 +53,5 @@ $languageStrings = array(
'заявление на лист' => 'заявление на лист', 'заявление на лист' => 'заявление на лист',
'урегулирование' => 'урегулирование', 'урегулирование' => 'урегулирование',
'заключение мирового соглашения' => 'заключение мирового соглашения', 'заключение мирового соглашения' => 'заключение мирового соглашения',
'Черновик' => 'Черновик',
); );

View File

@@ -1,20 +1,24 @@
/* AI Drawer - основные стили */ /* AI Drawer - основные стили */
.ai-drawer { .ai-drawer {
position: fixed; position: fixed;
right: -400px; /* Начально скрыт */ right: 0; /* Всегда прижат к правому краю */
top: 0; top: 0;
width: 400px; width: 400px;
min-width: 300px; /* Минимальная ширина */ min-width: 300px; /* Минимальная ширина */
max-width: 50vw; /* Максимальная ширина - половина экрана */ max-width: 50vw; /* Максимальная ширина - половина экрана */
height: 100vh; height: 100vh;
max-height: 100vh; /* Не превышаем высоту экрана */
background: #ffffff; /* Чистый белый фон */ background: #ffffff; /* Чистый белый фон */
box-shadow: -2px 0 15px rgba(0,0,0,0.1); box-shadow: -2px 0 15px rgba(0,0,0,0.1);
transition: right 0.3s ease; transform: translateX(100%); /* Начально скрыт - сдвинут вправо на 100% своей ширины */
transition: transform 0.3s ease;
z-index: 999999; z-index: 999999;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 14px; /* Базовый размер шрифта */ font-size: 14px; /* Базовый размер шрифта */
border-left: 1px solid #e9ecef; border-left: 1px solid #e9ecef;
overflow: hidden; /* Предотвращаем выход элементов за пределы */
box-sizing: border-box; /* Учитываем padding и border в ширине */
} }
/* Полоска для изменения ширины */ /* Полоска для изменения ширины */
@@ -44,13 +48,18 @@
user-select: none; user-select: none;
} }
/* Убираем transition при изменении размера, чтобы не было задержек */
.ai-drawer.resizing.open {
transform: translateX(0) !important;
}
.ai-drawer.resizing .ai-drawer-resize-handle { .ai-drawer.resizing .ai-drawer-resize-handle {
background: #007bff; background: #007bff;
width: 4px; width: 4px;
} }
.ai-drawer.open { .ai-drawer.open {
right: 0; transform: translateX(0); /* Показываем - сдвигаем на место */
} }
/* Скрываем кнопку AI когда drawer открыт */ /* Скрываем кнопку AI когда drawer открыт */
@@ -91,6 +100,9 @@ body.ai-drawer-open .ai-drawer-toggle {
align-items: center; align-items: center;
font-weight: 600; font-weight: 600;
border-bottom: 1px solid #0056b3; border-bottom: 1px solid #0056b3;
flex-shrink: 0; /* Не сжимается при изменении размера */
min-height: 50px; /* Минимальная высота для кнопки закрытия */
box-sizing: border-box;
} }
.ai-drawer-close { .ai-drawer-close {
@@ -436,12 +448,42 @@ body.ai-drawer-open .ai-drawer-toggle {
.ai-message-content p { .ai-message-content p {
margin: 0 0 5px 0; margin: 0 0 5px 0;
word-wrap: break-word;
word-break: break-word;
} }
.ai-message-content p:last-child { .ai-message-content p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
/* Стили для ссылок в сообщениях */
.ai-message-link {
color: #007bff;
text-decoration: underline;
cursor: pointer;
word-break: break-word;
display: inline-block;
margin: 2px 0;
padding: 2px 4px;
border-radius: 3px;
transition: all 0.2s ease;
}
.ai-message-link:hover {
color: #0056b3;
text-decoration: none;
background-color: #e7f3ff;
padding: 2px 6px;
}
.ai-message-link:visited {
color: #6f42c1;
}
.ai-message-link:active {
color: #004085;
}
.ai-message-time { .ai-message-time {
font-size: 11px; font-size: 11px;
color: #6c757d; /* Серый цвет для времени */ color: #6c757d; /* Серый цвет для времени */
@@ -456,6 +498,8 @@ body.ai-drawer-open .ai-drawer-toggle {
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
display: flex; display: flex;
gap: 10px; gap: 10px;
flex-shrink: 0; /* Не сжимается при изменении размера */
box-sizing: border-box;
align-items: center; align-items: center;
} }
@@ -510,7 +554,8 @@ body.ai-drawer-open .ai-drawer-toggle {
@media (max-width: 768px) { @media (max-width: 768px) {
.ai-drawer { .ai-drawer {
width: 100%; width: 100%;
right: -100%; right: 0;
transform: translateX(100%); /* Начально скрыт на мобильных */
height: 100vh; height: 100vh;
height: 100dvh; /* Динамическая высота viewport для мобильных */ height: 100dvh; /* Динамическая высота viewport для мобильных */
display: flex; display: flex;
@@ -731,7 +776,8 @@ body.ai-drawer-open .ai-drawer-toggle {
width: 400px; width: 400px;
min-width: 300px; min-width: 300px;
max-width: 50vw; max-width: 50vw;
right: -400px; right: 0;
transform: translateX(100%); /* Начально скрыт на планшетах */
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -48,7 +48,11 @@ class AIDrawer {
'<div class="ai-avatar assistant"></div>' + '<div class="ai-avatar assistant"></div>' +
'<div class="ai-message-content">' + '<div class="ai-message-content">' +
'<p>Привет! Я ваш AI ассистент. Чем могу помочь?</p>' + '<p>Привет! Я ваш AI ассистент. Чем могу помочь?</p>' +
'<div class="ai-message-time">' + new Date().toLocaleTimeString() + '</div>' + '<div class="ai-message-time">' + new Date().toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
hour12: false // 24-часовой формат
}) + '</div>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
@@ -128,7 +132,7 @@ class AIDrawer {
// Обработчик изменения размера окна - ограничиваем ширину если нужно // Обработчик изменения размера окна - ограничиваем ширину если нужно
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (this.drawerWidth > window.innerWidth / 2) { if (this.isOpen && this.drawerWidth > window.innerWidth / 2) {
const maxWidth = window.innerWidth / 2; const maxWidth = window.innerWidth / 2;
this.setDrawerWidth(maxWidth); this.setDrawerWidth(maxWidth);
} }
@@ -144,8 +148,18 @@ class AIDrawer {
const savedWidth = localStorage.getItem('ai-drawer-width'); const savedWidth = localStorage.getItem('ai-drawer-width');
if (savedWidth) { if (savedWidth) {
const width = parseInt(savedWidth, 10); const width = parseInt(savedWidth, 10);
if (width >= 300 && width <= window.innerWidth / 2) { const maxWidth = window.innerWidth / 2;
// Проверяем что ширина в допустимых пределах для текущего экрана
if (width >= 300 && width <= maxWidth) {
this.setDrawerWidth(width); this.setDrawerWidth(width);
} else if (width > maxWidth) {
// Если сохраненная ширина больше максимума - ограничиваем
console.log('AI Drawer: Saved width', width, 'exceeds max', maxWidth, ', adjusting');
this.setDrawerWidth(maxWidth);
} else {
// Если меньше минимума - устанавливаем минимум
console.log('AI Drawer: Saved width', width, 'is less than minimum, setting to 300');
this.setDrawerWidth(300);
} }
} }
@@ -220,6 +234,23 @@ class AIDrawer {
open() { open() {
console.log('AI Drawer: Opening drawer'); console.log('AI Drawer: Opening drawer');
if (this.drawer) { if (this.drawer) {
// Проверяем и корректируем ширину перед открытием
const maxWidth = window.innerWidth / 2;
if (this.drawerWidth > maxWidth) {
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to', maxWidth);
this.setDrawerWidth(maxWidth);
} else if (this.drawerWidth < 300) {
console.log('AI Drawer: Adjusting width from', this.drawerWidth, 'to 300');
this.setDrawerWidth(300);
}
// Убеждаемся что ширина применена к drawer
this.drawer.style.width = this.drawerWidth + 'px';
// Убеждаемся что drawer правильно позиционирован перед открытием
this.drawer.style.right = '0';
this.drawer.style.transform = 'translateX(0)';
this.drawer.classList.add('open'); this.drawer.classList.add('open');
} }
@@ -234,6 +265,33 @@ class AIDrawer {
mainContainer.setAttribute('data-drawer-width', this.drawerWidth); mainContainer.setAttribute('data-drawer-width', this.drawerWidth);
} }
// Прокручиваем вниз к последнему сообщению при открытии
const scrollToBottomOnOpen = () => {
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
if (drawerContent) {
const scroll = () => {
drawerContent.scrollTop = drawerContent.scrollHeight;
console.log('AI Drawer: Scrolled on open, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
};
// Прокручиваем последнее сообщение в видимую область
if (chatMessages && chatMessages.lastElementChild) {
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
requestAnimationFrame(() => {
scroll();
requestAnimationFrame(scroll);
});
}
};
// Прокручиваем после анимации открытия
setTimeout(scrollToBottomOnOpen, 300);
setTimeout(scrollToBottomOnOpen, 600);
// История уже загружена при инициализации страницы // История уже загружена при инициализации страницы
// Не нужно дополнительных запросов при открытии // Не нужно дополнительных запросов при открытии
} }
@@ -242,6 +300,10 @@ class AIDrawer {
console.log('AI Drawer: Closing drawer'); console.log('AI Drawer: Closing drawer');
if (this.drawer) { if (this.drawer) {
this.drawer.classList.remove('open'); this.drawer.classList.remove('open');
// Убеждаемся что drawer скрыт через transform
if (!this.drawer.classList.contains('open')) {
this.drawer.style.transform = 'translateX(100%)';
}
} }
document.body.classList.remove('ai-drawer-open'); document.body.classList.remove('ai-drawer-open');
@@ -295,6 +357,142 @@ class AIDrawer {
} }
} }
// Функция для определения читаемого текста ссылки на основе URL
getLinkText(url) {
const urlLower = url.toLowerCase();
const maxLength = 60; // Максимальная длина ссылки до замены
// Если ссылка короткая, показываем её полностью
if (url.length <= maxLength) {
return url;
}
// Анализируем URL и определяем тип действия
if (urlLower.includes('download') || urlLower.includes('file') || urlLower.includes('скачать')) {
return '📥 Скачать документ';
}
if (urlLower.includes('edit') || urlLower.includes('редактир') || urlLower.includes('onlyoffice')) {
return '✏️ Открыть для редактирования';
}
if (urlLower.includes('view') || urlLower.includes('просмотр') || urlLower.includes('preview')) {
return '👁️ Открыть для просмотра';
}
if (urlLower.includes('document') || urlLower.includes('документ') || urlLower.includes('.docx') || urlLower.includes('.pdf')) {
return '📄 Открыть документ';
}
if (urlLower.includes('create') || urlLower.includes('создать')) {
return ' Создать документ';
}
// Общие варианты для длинных ссылок
const linkTexts = [
'🔗 Открыть ссылку',
'👉 Смотреть здесь',
'📋 Подробнее',
'🔍 Перейти к документу',
'📎 Открыть'
];
// Выбираем случайный вариант для разнообразия
return linkTexts[Math.floor(Math.random() * linkTexts.length)];
}
// Функция для преобразования URL в кликабельные ссылки
convertUrlsToLinks(text) {
if (!text) return '';
let result = text;
// ШАГ 1: Обрабатываем Markdown ссылки [текст](url)
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
result = result.replace(markdownLinkRegex, (match, linkText, url) => {
// Проверяем, что это валидный URL
if (url.match(/^https?:\/\//i)) {
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
}
return match; // Если не URL, оставляем как есть
});
// ШАГ 2: Временно заменяем уже существующие HTML-ссылки на плейсхолдеры
const htmlLinks = [];
const htmlLinkRegex = /<a\s+[^>]*href\s*=\s*["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
result = result.replace(htmlLinkRegex, (match, href, linkText) => {
const placeholder = `__HTML_LINK_${htmlLinks.length}__`;
htmlLinks.push({ href, linkText, match });
return placeholder;
});
// ШАГ 3: Экранируем оставшийся HTML для безопасности
const escaped = result
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// ШАГ 4: Восстанавливаем HTML-ссылки (плейсхолдеры не экранированы, т.к. не содержат < >)
let finalResult = escaped;
htmlLinks.forEach((link, index) => {
const placeholder = `__HTML_LINK_${index}__`;
// Используем оригинальную ссылку, но добавляем класс если его нет
let htmlLink = link.match;
if (!htmlLink.includes('class=')) {
htmlLink = htmlLink.replace('<a', '<a class="ai-message-link"');
} else if (!htmlLink.includes('ai-message-link')) {
htmlLink = htmlLink.replace('class="', 'class="ai-message-link ');
htmlLink = htmlLink.replace("class='", "class='ai-message-link ");
}
// Убеждаемся что есть target="_blank"
if (!htmlLink.includes('target=')) {
htmlLink = htmlLink.replace('<a', '<a target="_blank" rel="noopener noreferrer"');
}
// Заменяем плейсхолдер на правильный HTML (не экранированный)
finalResult = finalResult.replace(placeholder, htmlLink);
});
// ШАГ 5: Преобразуем обычные URL в ссылки (только те, что не внутри уже существующих ссылок)
const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/gi;
let urlMatches = [];
let match;
// Сначала находим все URL и проверяем их контекст
while ((match = urlRegex.exec(finalResult)) !== null) {
const url = match[0];
const offset = match.index;
const beforeMatch = finalResult.substring(0, offset);
// Проверяем, нет ли открывающего тега <a перед этим URL
const lastOpenTag = beforeMatch.lastIndexOf('<a');
const lastCloseTag = beforeMatch.lastIndexOf('</a>');
// Если есть открывающий тег <a и нет закрывающего после него - значит мы внутри ссылки
if (lastOpenTag > lastCloseTag) {
continue; // Пропускаем, это уже часть ссылки
}
// Проверяем, не является ли это частью href атрибута
if (beforeMatch.lastIndexOf('href=') > lastCloseTag) {
continue; // Пропускаем, это часть href
}
urlMatches.push({ url, offset });
}
// Заменяем URL в обратном порядке (чтобы не сбить индексы)
for (let i = urlMatches.length - 1; i >= 0; i--) {
const { url, offset } = urlMatches[i];
const linkText = this.getLinkText(url);
const linkHtml = `<a href="${url}" target="_blank" rel="noopener noreferrer" class="ai-message-link" title="${url}">${linkText}</a>`;
finalResult = finalResult.substring(0, offset) + linkHtml + finalResult.substring(offset + url.length);
}
return finalResult;
}
addMessage(text, isUser = false, customTime = null) { addMessage(text, isUser = false, customTime = null) {
console.log('AI Drawer: addMessage called with:', {text: text.substring(0, 50), isUser, customTime}); console.log('AI Drawer: addMessage called with:', {text: text.substring(0, 50), isUser, customTime});
@@ -331,17 +529,69 @@ class AIDrawer {
contentDiv.className = 'ai-message-content'; contentDiv.className = 'ai-message-content';
const textDiv = document.createElement('p'); const textDiv = document.createElement('p');
textDiv.textContent = text; // Преобразуем URL в кликабельные ссылки
textDiv.innerHTML = this.convertUrlsToLinks(text);
contentDiv.appendChild(textDiv); contentDiv.appendChild(textDiv);
const timeDiv = document.createElement('div'); const timeDiv = document.createElement('div');
timeDiv.className = 'ai-message-time'; timeDiv.className = 'ai-message-time';
if (customTime) { if (customTime) {
// Если передано время из истории, используем его // Если передано время из истории, используем его
const historyTime = new Date(customTime); try {
timeDiv.textContent = historyTime.toLocaleTimeString(); // Логируем для отладки
console.log('AI Drawer: Parsing timestamp:', customTime);
const historyTime = new Date(customTime);
// Проверяем что дата валидна
if (isNaN(historyTime.getTime())) {
// Если дата невалидна, пытаемся распарсить как строку времени (старый формат)
console.warn('AI Drawer: Invalid timestamp format:', customTime, 'Parsed as:', historyTime);
timeDiv.textContent = customTime; // Показываем как есть
} else {
// Определяем, нужно ли показывать дату
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const messageDate = new Date(historyTime.getFullYear(), historyTime.getMonth(), historyTime.getDate());
const isToday = messageDate.getTime() === today.getTime();
let formattedTime;
if (isToday) {
// Если сообщение сегодня - показываем только время
formattedTime = historyTime.toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
} else {
// Если сообщение не сегодня - показываем дату и время
const dateStr = historyTime.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit'
});
const timeStr = historyTime.toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
formattedTime = `${dateStr} ${timeStr}`;
}
console.log('AI Drawer: Successfully formatted timestamp:', customTime, '->', formattedTime);
timeDiv.textContent = formattedTime;
}
} catch (error) {
console.error('AI Drawer: Error parsing timestamp:', customTime, error);
timeDiv.textContent = customTime || new Date().toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
hour12: false // 24-часовой формат
});
}
} else { } else {
timeDiv.textContent = new Date().toLocaleTimeString(); timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
hour12: false // 24-часовой формат
});
} }
contentDiv.appendChild(timeDiv); contentDiv.appendChild(timeDiv);
@@ -388,7 +638,11 @@ class AIDrawer {
const timeDiv = document.createElement('div'); const timeDiv = document.createElement('div');
timeDiv.className = 'ai-message-time'; timeDiv.className = 'ai-message-time';
timeDiv.textContent = new Date().toLocaleTimeString(); timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
hour12: false // 24-часовой формат
});
contentDiv.appendChild(timeDiv); contentDiv.appendChild(timeDiv);
messageDiv.appendChild(avatarDiv); messageDiv.appendChild(avatarDiv);
@@ -404,9 +658,12 @@ class AIDrawer {
streamText(element, text, speed = 30) { streamText(element, text, speed = 30) {
let index = 0; let index = 0;
let currentText = '';
const interval = setInterval(() => { const interval = setInterval(() => {
if (index < text.length) { if (index < text.length) {
element.textContent += text[index]; currentText += text[index];
// Преобразуем URL в кликабельные ссылки по мере добавления текста
element.innerHTML = this.convertUrlsToLinks(currentText);
index++; index++;
const content = this.drawer.querySelector('.ai-drawer-content'); const content = this.drawer.querySelector('.ai-drawer-content');
@@ -880,6 +1137,41 @@ class AIDrawer {
} }
}); });
// Прокручиваем вниз к последнему сообщению после загрузки истории
const scrollToBottom = () => {
const drawerContent = this.drawer?.querySelector('.ai-drawer-content');
const chatMessages = this.drawer?.querySelector('.ai-chat-messages');
if (drawerContent) {
// Способ 1: Прокручиваем контейнер
const scroll = () => {
drawerContent.scrollTop = drawerContent.scrollHeight;
console.log('AI Drawer: Scrolled container, scrollTop:', drawerContent.scrollTop, 'scrollHeight:', drawerContent.scrollHeight);
};
// Способ 2: Прокручиваем последнее сообщение в видимую область
if (chatMessages && chatMessages.lastElementChild) {
chatMessages.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end' });
console.log('AI Drawer: Scrolled last message into view');
}
// Используем requestAnimationFrame для более надежной прокрутки
requestAnimationFrame(() => {
scroll();
requestAnimationFrame(() => {
scroll();
});
});
} else {
console.warn('AI Drawer: Drawer content not found for scrolling');
}
};
// Прокручиваем с несколькими задержками для надежности
setTimeout(scrollToBottom, 100);
setTimeout(scrollToBottom, 300);
setTimeout(scrollToBottom, 600);
console.log('AI Drawer: Chat history restored -', data.history.length, 'messages'); console.log('AI Drawer: Chat history restored -', data.history.length, 'messages');
} else { } else {
console.log('AI Drawer: No chat history found. Response:', data); console.log('AI Drawer: No chat history found. Response:', data);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More