Добавлен field_label в результат переименования файлов, исправлена загрузка черновиков, обновлен формат пути S3 с project_name
This commit is contained in:
@@ -1,10 +1,65 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "ctx7sk-541e7992-c38f-442f-8902-ae99645f2477"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"context7": {
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
"headers": {
|
||||
"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"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -211,3 +211,9 @@ composer require phpoffice/phpword
|
||||
|
||||
Все компоненты реализованы, протестированы и готовы к использованию в n8n workflow.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -432,3 +432,9 @@ PUBLISH ai:response:task-xxx {"success": true, "documentUrl": "..."}
|
||||
- Ошибки изолированы
|
||||
- Легко отлаживать
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -197,3 +197,9 @@ https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_
|
||||
- Документ сразу доступен для редактирования в OnlyOffice
|
||||
- Путь формируется автоматически: `{module}/{recordName}_{recordId}/{fileName}.{ext}`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -244,3 +244,9 @@ _курсив_
|
||||
3. **Гибкость** — можно комбинировать элементы
|
||||
4. **Автоматическое форматирование** — документ получается красивым без ручной правки
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -152,3 +152,9 @@ curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
- [OnlyOffice Integration](https://api.onlyoffice.com/)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -246,3 +246,9 @@ echo json_encode(['success' => true, 'templates' => $templates]);
|
||||
3. **Решение:** Использовать WebDAV PROPFIND для получения списка файлов из папки Templates
|
||||
4. **Статус:** Наш текущий подход (WebDAV + PHPWord) является правильным и оптимальным решением
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -133,3 +133,9 @@ curl "https://office.clientright.ru:8443/index.php/apps/onlyoffice/ajax/template
|
||||
3. Протестировать получение списка через `list_templates.php`
|
||||
4. Использовать шаблоны через `create_from_template.php`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -115,10 +115,34 @@ try {
|
||||
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 = [
|
||||
'type' => isset($item['sender_type']) && $item['sender_type'] === 'user' ? 'user' : 'assistant',
|
||||
'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'] ?? '',
|
||||
'dialog_id' => $item['dialog_id'] ?? ''
|
||||
];
|
||||
@@ -131,7 +155,7 @@ try {
|
||||
$history[] = [
|
||||
'type' => 'assistant',
|
||||
'message' => "Привет! Я ваш AI ассистент. Работаем с '{$projectName}'. Чем могу помочь?",
|
||||
'timestamp' => date('H:i:s'),
|
||||
'timestamp' => date('c'), // ISO 8601 формат
|
||||
'id' => 'welcome-' . time(),
|
||||
'dialog_id' => 'new-dialog'
|
||||
];
|
||||
|
||||
175
include/Webservices/CreateClientProject.php
Normal file
175
include/Webservices/CreateClientProject.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,16 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
|
||||
|
||||
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(
|
||||
'delay_flight' => 'Задержка рейса',
|
||||
@@ -108,8 +118,8 @@ function vtws_createwebclaim($title, $contact_id, $project_id, $event_type, $des
|
||||
'parent_id' => '11x67458', // Заявитель - контрагент
|
||||
'ticketcategories' => $ticketCategory,
|
||||
'ticketstatus' => 'рассмотрение',
|
||||
'contact_id' => '12x'.$contact_id,
|
||||
'cf_2066' => '33x'.$project_id, // Связь с проектом
|
||||
'contact_id' => $contactWsId,
|
||||
'cf_2066' => $projectWsId, // Связь с проектом
|
||||
'ticketpriorities' => 'High',
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id),
|
||||
'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;
|
||||
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)
|
||||
$output = array(
|
||||
'ticket_id' => $ticketId,
|
||||
|
||||
@@ -52,27 +52,28 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
|
||||
// Валидация: убираем пробелы из номера полиса
|
||||
$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);
|
||||
|
||||
global $adb, $current_user;
|
||||
|
||||
$isNew = false; // Флаг: создан ли проект сейчас
|
||||
|
||||
// Проверяем существование проекта по номеру полиса И привязке к контакту
|
||||
// (т.к. по одному полису может быть несколько застрахованных лиц)
|
||||
// Проверяем существование проекта по номеру полиса И прямой привязке к контакту
|
||||
// (без зависимости от заполнения vtiger_crmentityrel)
|
||||
$query = "SELECT p.projectid
|
||||
FROM vtiger_project p
|
||||
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
|
||||
LEFT 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 = ?)
|
||||
INNER JOIN vtiger_crmentity e ON e.crmid = p.projectid
|
||||
WHERE e.deleted = 0
|
||||
AND pcf.cf_1885 = ?
|
||||
AND rel.crmid IS NOT NULL
|
||||
AND p.linktoaccountscontacts = ?
|
||||
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) {
|
||||
// Проект существует - ПРОСТО ВОЗВРАЩАЕМ ID (НЕ обновляем!)
|
||||
@@ -90,7 +91,7 @@ function vtws_createwebproject($policy_number, $contact_id, $period_start = '',
|
||||
'projectname' => $projectname,
|
||||
'projectstatus' => 'модерация',
|
||||
'projecttype' => 'ерв урегулирование',
|
||||
'linktoaccountscontacts' => '12x'.$contact_id, // Привязка к контакту
|
||||
'linktoaccountscontacts' => $contactIdWithPrefix, // Привязка к контакту
|
||||
'cf_1994' => '11x67458', // Заявитель (контрагент record=67458)
|
||||
'cf_1885' => $policy_number, // Номер полиса
|
||||
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id)
|
||||
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -53,4 +53,5 @@ $languageStrings = array(
|
||||
'заявление на лист' => 'заявление на лист',
|
||||
'урегулирование' => 'урегулирование',
|
||||
'заключение мирового соглашения' => 'заключение мирового соглашения',
|
||||
'Черновик' => 'Черновик',
|
||||
);
|
||||
@@ -1,20 +1,24 @@
|
||||
/* AI Drawer - основные стили */
|
||||
.ai-drawer {
|
||||
position: fixed;
|
||||
right: -400px; /* Начально скрыт */
|
||||
right: 0; /* Всегда прижат к правому краю */
|
||||
top: 0;
|
||||
width: 400px;
|
||||
min-width: 300px; /* Минимальная ширина */
|
||||
max-width: 50vw; /* Максимальная ширина - половина экрана */
|
||||
height: 100vh;
|
||||
max-height: 100vh; /* Не превышаем высоту экрана */
|
||||
background: #ffffff; /* Чистый белый фон */
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px; /* Базовый размер шрифта */
|
||||
border-left: 1px solid #e9ecef;
|
||||
overflow: hidden; /* Предотвращаем выход элементов за пределы */
|
||||
box-sizing: border-box; /* Учитываем padding и border в ширине */
|
||||
}
|
||||
|
||||
/* Полоска для изменения ширины */
|
||||
@@ -44,13 +48,18 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Убираем transition при изменении размера, чтобы не было задержек */
|
||||
.ai-drawer.resizing.open {
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
.ai-drawer.resizing .ai-drawer-resize-handle {
|
||||
background: #007bff;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.ai-drawer.open {
|
||||
right: 0;
|
||||
transform: translateX(0); /* Показываем - сдвигаем на место */
|
||||
}
|
||||
|
||||
/* Скрываем кнопку AI когда drawer открыт */
|
||||
@@ -91,6 +100,9 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #0056b3;
|
||||
flex-shrink: 0; /* Не сжимается при изменении размера */
|
||||
min-height: 50px; /* Минимальная высота для кнопки закрытия */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ai-drawer-close {
|
||||
@@ -436,12 +448,42 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
|
||||
.ai-message-content p {
|
||||
margin: 0 0 5px 0;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ai-message-content p:last-child {
|
||||
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 {
|
||||
font-size: 11px;
|
||||
color: #6c757d; /* Серый цвет для времени */
|
||||
@@ -456,6 +498,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0; /* Не сжимается при изменении размера */
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -510,7 +554,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
@media (max-width: 768px) {
|
||||
.ai-drawer {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
right: 0;
|
||||
transform: translateX(100%); /* Начально скрыт на мобильных */
|
||||
height: 100vh;
|
||||
height: 100dvh; /* Динамическая высота viewport для мобильных */
|
||||
display: flex;
|
||||
@@ -731,7 +776,8 @@ body.ai-drawer-open .ai-drawer-toggle {
|
||||
width: 400px;
|
||||
min-width: 300px;
|
||||
max-width: 50vw;
|
||||
right: -400px;
|
||||
right: 0;
|
||||
transform: translateX(100%); /* Начально скрыт на планшетах */
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -48,7 +48,11 @@ class AIDrawer {
|
||||
'<div class="ai-avatar assistant"></div>' +
|
||||
'<div class="ai-message-content">' +
|
||||
'<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>' +
|
||||
@@ -128,7 +132,7 @@ class AIDrawer {
|
||||
|
||||
// Обработчик изменения размера окна - ограничиваем ширину если нужно
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.drawerWidth > window.innerWidth / 2) {
|
||||
if (this.isOpen && this.drawerWidth > window.innerWidth / 2) {
|
||||
const maxWidth = window.innerWidth / 2;
|
||||
this.setDrawerWidth(maxWidth);
|
||||
}
|
||||
@@ -144,8 +148,18 @@ class AIDrawer {
|
||||
const savedWidth = localStorage.getItem('ai-drawer-width');
|
||||
if (savedWidth) {
|
||||
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);
|
||||
} 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() {
|
||||
console.log('AI Drawer: Opening 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');
|
||||
}
|
||||
|
||||
@@ -234,6 +265,33 @@ class AIDrawer {
|
||||
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');
|
||||
if (this.drawer) {
|
||||
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');
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
// ШАГ 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) {
|
||||
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';
|
||||
|
||||
const textDiv = document.createElement('p');
|
||||
textDiv.textContent = text;
|
||||
// Преобразуем URL в кликабельные ссылки
|
||||
textDiv.innerHTML = this.convertUrlsToLinks(text);
|
||||
contentDiv.appendChild(textDiv);
|
||||
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'ai-message-time';
|
||||
if (customTime) {
|
||||
// Если передано время из истории, используем его
|
||||
const historyTime = new Date(customTime);
|
||||
timeDiv.textContent = historyTime.toLocaleTimeString();
|
||||
try {
|
||||
// Логируем для отладки
|
||||
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 {
|
||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||||
timeDiv.textContent = new Date().toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false // 24-часовой формат
|
||||
});
|
||||
}
|
||||
contentDiv.appendChild(timeDiv);
|
||||
|
||||
@@ -388,7 +638,11 @@ class AIDrawer {
|
||||
|
||||
const timeDiv = document.createElement('div');
|
||||
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);
|
||||
|
||||
messageDiv.appendChild(avatarDiv);
|
||||
@@ -404,9 +658,12 @@ class AIDrawer {
|
||||
|
||||
streamText(element, text, speed = 30) {
|
||||
let index = 0;
|
||||
let currentText = '';
|
||||
const interval = setInterval(() => {
|
||||
if (index < text.length) {
|
||||
element.textContent += text[index];
|
||||
currentText += text[index];
|
||||
// Преобразуем URL в кликабельные ссылки по мере добавления текста
|
||||
element.innerHTML = this.convertUrlsToLinks(currentText);
|
||||
index++;
|
||||
|
||||
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');
|
||||
} else {
|
||||
console.log('AI Drawer: No chat history found. Response:', data);
|
||||
|
||||
BIN
s3/ERV/398128/398131_Polis.pdf
Normal file
BIN
s3/ERV/398128/398131_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398128/398133_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398128/398133_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398128/398135/398136_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398128/398135/398136_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398235/398238_Polis.pdf
Normal file
BIN
s3/ERV/398235/398238_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398235/398240_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398235/398240_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398235/398242_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398235/398242_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398235/398244/398245_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398235/398244/398245_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398253/398256_Polis.pdf
Normal file
BIN
s3/ERV/398253/398256_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398253/398258_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398253/398258_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398253/398260_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398253/398260_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398253/398262/398263_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398253/398262/398263_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398275/398278_Polis.pdf
Normal file
BIN
s3/ERV/398275/398278_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398275/398280_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398275/398280_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398275/398282/398283_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398275/398282/398283_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398294/398297_Polis.pdf
Normal file
BIN
s3/ERV/398294/398297_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398294/398299_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398294/398299_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398294/398301/398302_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398294/398301/398302_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398311/398314_Polis.pdf
Normal file
BIN
s3/ERV/398311/398314_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398311/398316_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398311/398316_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398311/398318/398319_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398311/398318/398319_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398327/398330_Polis.pdf
Normal file
BIN
s3/ERV/398327/398330_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398327/398332_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398327/398332_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398327/398334/398335_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398327/398334/398335_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398372/398375_Polis.pdf
Normal file
BIN
s3/ERV/398372/398375_Polis.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398372/398377_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398372/398377_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398372/398379/398380_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398372/398379/398380_Podtverzhdayuschie_dokumenty.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
s3/ERV/398620/398623_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
BIN
s3/ERV/398620/398623_Dokument_udostoveryayuschiy_lichnost.pdf
Normal file
Binary file not shown.
BIN
s3/ERV/398620/398625/398626_Podtverzhdayuschie_dokumenty.pdf
Normal file
BIN
s3/ERV/398620/398625/398626_Podtverzhdayuschie_dokumenty.pdf
Normal file
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
storage/2025/November/week2/398077_7 заявление потребителя.pdf
Normal file
BIN
storage/2025/November/week2/398077_7 заявление потребителя.pdf
Normal file
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.
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.
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
Reference in New Issue
Block a user