dolibarr 25.0.0-alpha
ai.lib.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2022 Alice Adminson <aadminson@example.com>
3 * Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
5 * Copyright (C) 2026 Anthony Damhet <a.damhet@progiseize.fr>
6 * Copyright (C) 2026 Nick Fragoulis
7 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
8 *
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY, without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
29include_once DOL_DOCUMENT_ROOT.'/ai/class/ai.class.php';
30
31
38{
39 global $langs;
40
41 $arrayofaifeatures = array(
42 'textgenerationemail' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("EmailContent").')', 'picto' => '', 'status' => 'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_EMAIL),
43 'textgenerationwebpage' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("WebsitePage").')', 'picto' => '', 'status' => 'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_WEBPAGE),
44 'textgeneration' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("Other").')', 'picto' => '', 'status' => 'notused', 'function' => 'TEXT'),
45
46 'texttranslation' => array('label' => $langs->trans('TextTranslation'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION),
47 'textsummarize' => array('label' => $langs->trans('TextSummarize'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE),
48 'textspellchecker' => array('label' => $langs->trans('TextSpellChecker'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_SPELLCHECKER),
49 'textrephrase' => array('label' => $langs->trans('TextRephraser'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER),
50
51 'textgenerationextrafield' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("ExtrafieldFiller").')', 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER),
52
53 'imagegeneration' => array('label' => 'ImageGeneration', 'picto' => '', 'status' => 'notused', 'function' => 'IMAGE'),
54 'videogeneration' => array('label' => 'VideoGeneration', 'picto' => '', 'status' => 'notused', 'function' => 'VIDEO'),
55 'audiogeneration' => array('label' => 'AudioGeneration', 'picto' => '', 'status' => 'notused', 'function' => 'AUDIO'),
56 'transcription' => array('label' => 'AudioTranscription', 'picto' => '', 'status' => 'notused', 'function' => 'TRANSCRIPT'),
57 'translation' => array('label' => 'AudioTranslation', 'picto' => '', 'status' => 'notused', 'function' => 'TRANSLATE'),
58 'docparsing' => array('label' => 'DocumentParsing', 'picto' => '', 'status' => 'experimental', 'function' => 'DOCPARSING')
59 );
60
61 return $arrayofaifeatures;
62}
63
70{
71 global $langs;
72
73 $arrayofai = array(
74 '-1' => array('label' => $langs->trans('SelectAService')),
75 'chatgpt' => array(
76 'label' => 'ChatGPT (OpenAI)',
77 'url' => 'https://api.openai.com/v1/',
78 'setup' => 'https://platform.openai.com/account/api-keys',
79 'textgeneration' => array('default' => 'gpt-5.2'), // Flagship model released late 2025, updated Feb 2026
80 'imagegeneration' => array('default' => 'gpt-image-1.5'), // Replaced DALL-E 3; 4x faster and native to GPT-5
81 'audiogeneration' => array('default' => 'gpt-audio-1.5'), // New Feb 23, 2026 release for high-fidelity audio out
82 'videogeneration' => array('default' => 'sora-2'), // OpenAI's standard API video model
83 'transcription' => array('default' => 'whisper-large-v3-turbo'), // The current speed/accuracy benchmark for ASR
84 'translation' => array('default' => 'whisper-large-v3-turbo'), // Still the best for multi-language audio translation
85 'docparsing' => array('default' => 'gpt-5.2'), // Uses the new Responses API / Vision capabilities
86 'adapter_type' => 'openai'
87 ),
88 'groq' => array(
89 'label' => 'Groq (LPU Inference)',
90 'url' => 'https://api.groq.com/openai/v1/',
91 'setup' => 'https://console.groq.com/keys',
92 'textgeneration' => array('default' => 'llama-4-8b-instant'), // February 2026 flagship for extreme speed (1,000+ t/s)
93 'imagegeneration' => array('default' => 'na'),
94 'audiogeneration' => array('default' => 'na'),
95 'videogeneration' => array('default' => 'na'),
96 'transcription' => array('default' => 'whisper-large-v3-turbo'), // Groq's specialized high-speed Whisper implementation
97 'translation' => array('default' => 'whisper-large-v3-turbo'), // High-speed audio translation to English
98 'docparsing' => array('default' => 'llama-4-70b-versatile'), // Best for structured data extraction from text
99 'adapter_type' => 'openai'
100 ),
101 'mistral' => array(
102 'label' => 'Mistral AI',
103 'url' => 'https://api.mistral.ai/v1/',
104 'setup' => 'https://console.mistral.ai/api-keys/',
105 'textgeneration' => array('default' => 'mistral-small-latest', 'examples' => 'mistral-tiny-latest, mistral-small-latest, mistral-medium-latest, mistral-large-latest'), // Points to Mistral Small 3 (updated Feb 2026)
106 'imagegeneration' => array('default' => 'na'),
107 'audiogeneration' => array('default' => 'na'),
108 'videogeneration' => array('default' => 'na'),
109 'transcription' => array('default' => 'na'),
110 'translation' => array('default' => 'na'),
111 'docparsing' => array('default' => 'pixtral-12b-latest'), // Mistral's native vision/doc model
112 'adapter_type' => 'openai'
113 ),
114 'deepseek' => array(
115 'label' => 'DeepSeek',
116 'url' => 'https://api.deepseek.com',
117 'setup' => 'https://platform.deepseek.com/api_keys',
118 'textgeneration' => array('default' => 'deepseek-v4'), // Released Feb 2026, flagship MoE model
119 'imagegeneration' => array('default' => 'deepseek-janus-2'), // DeepSeek's latest multimodal vision/gen model
120 'audiogeneration' => array('default' => 'na'),
121 'videogeneration' => array('default' => 'na'),
122 'transcription' => array('default' => 'na'),
123 'translation' => array('default' => 'na'),
124 'docparsing' => array('default' => 'deepseek-v4'), // Massive 1M context support for parsing
125 'adapter_type' => 'openai'
126 ),
127 'perplexity' => array(
128 'label' => 'Perplexity (Sonar)',
129 'url' => 'https://api.perplexity.ai',
130 'setup' => 'https://www.perplexity.ai/settings/api',
131 'textgeneration' => array('default' => 'sonar-pro'), // Flagship search model as of Feb 2026
132 'imagegeneration' => array('default' => 'na'),
133 'audiogeneration' => array('default' => 'na'),
134 'videogeneration' => array('default' => 'na'),
135 'transcription' => array('default' => 'na'),
136 'translation' => array('default' => 'na'),
137 'docparsing' => array('default' => 'sonar-reasoning'), // Best for analyzing search-grounded docs
138 'adapter_type' => 'openai'
139 ),
140 'zai' => array(
141 'label' => 'Zhipu AI (GLM)',
142 'url' => 'https://api.z.ai/api/paas/v4',
143 'setup' => 'https://docs.z.ai/guides/overview/quick-start',
144 'textgeneration' => array('default' => 'glm-5'), // Flagship released February 11, 2026
145 'imagegeneration' => array('default' => 'cogview-4'), // Zhipu's latest SOTA image generator
146 'audiogeneration' => array('default' => 'cogvlm2-audio'), // High-fidelity conversational audio
147 'videogeneration' => array('default' => 'cogvideox-2'), // Flagship API video model
148 'transcription' => array('default' => 'na'),
149 'translation' => array('default' => 'na'),
150 'docparsing' => array('default' => 'glm-5'), // Top-tier agentic document processing
151 'adapter_type' => 'openai'
152 ),
153 'custom' => array(
154 'label' => 'Custom',
155 'url' => 'https://domainofapi.com/v1/',
156 'setup' => 'Ask your AI provider how to get your API key',
157 'textgeneration' => array('default' => 'tinyllama-1.1b'),
158 'imagegeneration' => array('default' => 'mixtral-8x7b-32768'),
159 'audiogeneration' => array('default' => 'mixtral-8x7b-32768'),
160 'videogeneration' => array('default' => 'na'),
161 'transcription' => array('default' => 'mixtral-8x7b-32768'),
162 'translation' => array('default' => 'mixtral-8x7b-32768'),
163 'docparsing' => array('default' => 'na'),
164 'adapter_type' => 'openai'
165 ),
166 // --- SPECIALIZED ADAPTERS ---
167 'anthropic' => array(
168 'label' => 'Anthropic (Claude)',
169 'url' => 'https://api.anthropic.com/v1/',
170 'setup' => 'https://console.anthropic.com/',
171 'textgeneration' => array('default' => 'claude-opus-4-6'), // Released Feb 2026; features a 1M context window
172 'imagegeneration' => array('default' => 'na'), // Anthropic remains focused on text/code logic
173 'audiogeneration' => array('default' => 'na'),
174 'videogeneration' => array('default' => 'na'),
175 'transcription' => array('default' => 'na'),
176 'translation' => array('default' => 'na'),
177 'docparsing' => array('default' => 'claude-opus-4-6'), // Leading model for "Computer Use" and PDF analysis
178 'adapter_type' => 'anthropic'
179 ),
180 'google' => array(
181 'label' => 'Google Gemini',
182 'url' => 'https://generativelanguage.googleapis.com/v1beta/',
183 'setup' => 'https://aistudio.google.com/',
184 'textgeneration' => array('default' => 'gemini-3.1-pro-preview'), // Flagship reasoning model released Feb 19, 2026
185 'imagegeneration' => array('default' => 'nano-banana-pro'), // Latest SOTA image model (Gemini 3 Pro Image)
186 'audiogeneration' => array('default' => 'gemini-2.5-pro-tts'), // High-fidelity native speech synthesis
187 'videogeneration' => array('default' => 'veo-3.1'), // Google's flagship cinematic video API
188 'transcription' => array('default' => 'gemini-3.1-pro-preview'), // Native multi-modal audio reasoning
189 'translation' => array('default' => 'gemini-3.1-pro-preview'), // Native audio-to-text translation
190 'docparsing' => array('default' => 'gemini-3.1-pro-preview'), // Massive 2M+ context window for full repo parsing
191 'adapter_type' => 'google'
192 )
193 );
194
195 return $arrayofai;
196}
197
213function testAIConnection(string $service, string $key, string $url): array
214{
215 if (empty($key)) {
216 return ['success' => false, 'message' => 'API Key is empty'];
217 }
218
219 // Load Defaults (Ensure this function exists or handle the error)
220 if (!function_exists('getListOfAIServices')) {
221 return ['success' => false, 'message' => 'Configuration helper function missing.'];
222 }
223
224 $list = getListOfAIServices();
225 $defUrl = $list[$service]['url'] ?? '';
226 // Use model from config, fallback to hardcoded if necessary
227 $defaultModel = $list[$service]['model'] ?? 'unknown';
228
229 // Normalize URL
230 if (empty($url)) {
231 $url = $defUrl;
232 }
233 $url = rtrim($url, '/');
234
235 $data = [];
236 $headers = ["Content-Type: application/json"];
237
238 $model = '';
239 if (empty($model)) {
240 $model = getDolGlobalString('AI_API_' . strtoupper($service) . '_MODEL_TEXT');
241 }
242
243 // GOOGLE
244 if ($service == 'google' || strpos($url, 'googleapis') !== false) {
245 if (strpos($url, ':generateContent') === false) {
246 if (strpos($url, 'models') === false) {
247 $url .= "/models/$model:generateContent";
248 } else {
249 $url .= "/$model:generateContent";
250 }
251 }
252 $url .= "?key=" . $key;
253 $data = ["contents" => [ ["parts" => [ ["text" => "Hello"] ] ] ], "generationConfig" => ["maxOutputTokens" => 5]];
254 } elseif ($service == 'anthropic' || strpos($url, 'anthropic') !== false) { // ANTHROPIC
255 if (strpos($url, 'messages') === false) $url .= '/messages';
256 $headers[] = "x-api-key: $key";
257 $headers[] = "anthropic-version: 2023-06-01";
258 $data = [
259 "model" => $model, // Uses Configured Model
260 "messages" => [["role" => "user", "content" => "Hello"]],
261 "max_tokens" => 5
262 ];
263 } else {
264 if (strpos($url, '/chat/completions') === false) $url .= '/chat/completions';
265 $headers[] = "Authorization: Bearer $key";
266
267 $data = [
268 "model" => $model, // Uses Configured Model (from Priority Chain)
269 "messages" => [["role" => "user", "content" => "Hello"]],
270 "max_tokens" => 5
271 ];
272 }
273
274 // Execute cURL
275 $ch = curl_init();
276 curl_setopt($ch, CURLOPT_URL, $url);
277 curl_setopt($ch, CURLOPT_POST, true);
278 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
279 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
280 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
281 curl_setopt($ch, CURLOPT_TIMEOUT, 10);
282 // Optional: Add SSL verification if behind a proxy with self-signed certs
283 // curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
284
285 $result = curl_exec($ch);
286 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
287 $err = curl_error($ch);
288 curl_close($ch);
289
290 if ($err) {
291 return ['success' => false, 'message' => "Curl Error: $err"];
292 }
293
294 if ($httpCode >= 200 && $httpCode < 300) {
295 return ['success' => true, 'message' => "OK (HTTP $httpCode)."];
296 } else {
297 $json = json_decode($result, true);
298 // Attempt to find the error message in various common structures
299 $msg = $json['error']['message'] ?? $json['message'] ?? substr($result, 0, 150);
300 return ['success' => false, 'message' => "HTTP $httpCode. Error: $msg"];
301 }
302}
303
320function ai_log_request($db, $user, $query, array $response, $provider, float $time, float $confidence, $status, $error = '', $rawReq = '', $rawRes = '')
321{
322 global $conf;
323
324 if (!getDolGlobalInt('AI_LOG_REQUESTS')) {
325 return 0;
326 }
327
328 $tool = isset($response['tool']) ? (string) $response['tool'] : '';
329
330 if (dol_strlen($rawReq) > 60000) {
331 $rawReq = dol_substr($rawReq, 0, 60000) . '... [Truncated]';
332 }
333
334 $rawResStr = (string) $rawRes;
335 if (dol_strlen($rawResStr) > 60000) {
336 $rawResStr = dol_substr($rawResStr, 0, 60000) . '... [Truncated]';
337 }
338
339 $sql = "INSERT INTO " . MAIN_DB_PREFIX . "ai_request_log (";
340 $sql .= "entity, date_request, fk_user, query_text, tool_name, provider, ";
341 $sql .= "execution_time, confidence, status, error_msg, raw_request_payload, raw_response_payload";
342 $sql .= ") VALUES (";
343 $sql .= ((int) $conf->entity) . ", ";
344 $sql .= "'" . $db->idate(dol_now()) . "', ";
345 $sql .= ((int) $user->id) . ", ";
346 $sql .= "'" . $db->escape($query) . "', ";
347 $sql .= "'" . $db->escape($tool) . "', ";
348 $sql .= "'" . $db->escape($provider) . "', ";
349 $sql .= ((float) $time) . ", ";
350 $sql .= ((float) $confidence) . ", ";
351 $sql .= "'" . $db->escape($status) . "', ";
352 $sql .= "'" . $db->escape($error) . "', ";
353 $sql .= "'" . $db->escape($rawReq) . "', ";
354 $sql .= "'" . $db->escape($rawResStr) . "'";
355 $sql .= ")";
356
357 $resql = $db->query($sql);
358 if (!$resql) {
360 }
361
362 return 0;
363}
364
371{
372 $arrayforaisummarize = array(
373 //'20_w' => 'SummarizeTwentyWords',
374 '50_w' => 'SummarizeFiftyWords',
375 '100_w' => 'SummarizeHundredWords',
376 '200_w' => 'SummarizeTwoHundredWords',
377 '1_p' => 'SummarizeOneParagraphs',
378 '2_p' => 'SummarizeTwoParagraphs',
379 '25_pc' => 'SummarizeTwentyFivePercent',
380 '50_pc' => 'SummarizeFiftyPercent',
381 '75_pc' => 'SummarizeSeventyFivePercent'
382 );
383
384 return $arrayforaisummarize;
385}
386
393{
394 $arrayforaierephrasestyle = array(
395 'spellchecker' => 'RephraseSpellChecker',
396 'professional' => 'RephraseStyleProfessional',
397 'humouristic' => 'RephraseStyleHumouristic',
398 );
399
400 return $arrayforaierephrasestyle;
401}
402
409{
410 global $langs, $conf;
411
412 $langs->load("agenda");
413
414 $h = 0;
415 $head = array();
416
417 $head[$h][0] = dol_buildpath("/ai/admin/setup.php", 1);
418 $head[$h][1] = $langs->trans("Settings");
419 $head[$h][2] = 'settings';
420 $h++;
421
422 $head[$h][0] = dol_buildpath("/ai/admin/custom_prompt.php", 1);
423 $head[$h][1] = $langs->trans("CustomPrompt");
424 $head[$h][2] = 'custom';
425 $h++;
426
427 if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 2) {
428 $head[$h][0] = dol_buildpath("/ai/admin/assistant.php", 1);
429 $head[$h][1] = $langs->trans("Assistant");
430 $head[$h][2] = 'assistant';
431 $h++;
432 }
433
434 if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 2) {
435 $head[$h][0] = dol_buildpath("/ai/admin/server_mcp.php", 1);
436 $head[$h][1] = $langs->trans("MCPServer");
437 $head[$h][2] = 'servermcp';
438 $h++;
439 }
440
441 if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 2) {
442 $head[$h][0] = dol_buildpath("/ai/admin/configure_tools.php", 1);
443 $head[$h][1] = $langs->trans("ToolAccessControl");
444 $head[$h][2] = 'tools';
445 $h++;
446 }
447
448 /*
449 $head[$h][0] = dol_buildpath("/ai/admin/myobject_extrafields.php", 1);
450 $head[$h][1] = $langs->trans("ExtraFields");
451 $head[$h][2] = 'myobject_extrafields';
452 $h++;
453 */
454
455 // Show more tabs from modules
456 // Entries must be declared in modules descriptor with line
457 //$this->tabs = array(
458 // 'entity:+tabname:Title:@ai:/ai/mypage.php?id=__ID__'
459 //); // to add new tab
460 //$this->tabs = array(
461 // 'entity:-tabname:Title:@ai:/ai/mypage.php?id=__ID__'
462 //); // to remove a tab
463 complete_head_from_modules($conf, $langs, null, $head, $h, 'ai@ai');
464
465 complete_head_from_modules($conf, $langs, null, $head, $h, 'ai@ai', 'remove');
466
467 return $head;
468}
469
479{
480 $serviceKey = getDolGlobalString('AI_API_SERVICE');
481 if (empty($serviceKey) || $serviceKey === '-1') {
482 return '';
483 }
484
485 $services = getListOfAIServices();
486
487 return isset($services[$serviceKey]['label']) ? (string) $services[$serviceKey]['label'] : (string) $serviceKey;
488}
489
497{
498 global $langs, $user;
499
500 $keys = array(
501 // General UI
502 'NoDataAvailable',
503 'Error',
504 'NoRecordFound',
505 'Download',
506 'Show',
507 'Confirm',
508 'ConfirmAiAction',
509 'ClearChatHistoryTitle',
510 'HistoryCleared',
511 'Send',
512 'TypeYourQuestion',
513
514 // Placeholders & Status
515 'TypeOrSpeak',
516 'UploadLocalDoc',
517 'UploadCloudDoc',
518 'DocLoaded',
519 'Listening',
520 'Transcribed',
521 'NoSpeech',
522 'ProcessingAudio',
523 'Timeout',
524 'Cancelled',
525
526 // Engine Specific
527 'CloudSpeechReady',
528 'WhisperReady',
529 'DownloadingModel',
530 'ModelLoading',
531
532 // Document Processing
533 'ProcessingFile',
534 'ReadingPdf',
535 'PdfError',
536 'UnsupportedFileType',
537 'TryingOCR',
538 'OcrProgress',
539 'SwitchingAIModel',
540 'OcrFailed',
541 'ReadingWord',
542 'ReadingExcel',
543 'ReadingOdf',
544
545 // Errors
546 'MicError',
547 'MicTooQuiet',
548 'ConnectionBlocked',
549 'ConnectionBlockedHelp',
550 'WorkerInitFailed',
551 'NetworkError',
552 'AIError',
553 'EmptyAIResponse',
554 'BrowserNotSupported',
555
556 // Actions & Dialogs
557 'YesProceed',
558 'Cancel',
559 'Submit',
560 'ActionCancelled',
561 'ExecutingTool',
562 'FetchingData',
563 'GeneratingLink',
564 'Found',
565 'TypeResponse',
566 'OpenVerb',
567
568 // Voice Confirmation
569 'VoiceYesNo',
570 'VoiceQuiet',
571 'PleaseRepeat',
572 'HeardText',
573
574 // Context
575 'DocContextIntro',
576 'DocContextOutro',
577
578 // Model picker
579 'AIModelAuto',
580 'AIModelFast',
581 'AIModelBalanced',
582 'AIModelDeep',
583 'AIModelSavedGone'
584 );
585
586 $ai_translations = array();
587 foreach ($keys as $key) {
588 $ai_translations[$key] = $langs->transnoentitiesnoconv($key);
589 }
590 $ai_translations['DownloadPdf'] = $langs->transnoentitiesnoconv("Download").' PDF';
591 $ai_translations['CloudVoiceRequiresSecureContext'] = $langs->trans(
592 "CloudVoiceRequiresSecureContext",
593 "HTTPS",
594 "localhost",
595 "Whisper"
596 );
597
598 // First letter of the current user name, shown in the "user" message avatar
599 $userinitial = '';
600 if (is_object($user)) {
601 $namesource = $user->firstname ? $user->firstname : ($user->login ? $user->login : '');
602 $userinitial = dol_strtoupper(dol_substr($namesource, 0, 1));
603 }
604
605 return array(
606 'mode' => getDolGlobalString('AI_DEFAULT_INPUT_MODE'),
607 'labels' => $ai_translations,
608 // Endpoints are called with absolute URLs so the chat also works when
609 // injected into another page (topbar popover) and not only when served
610 // from /ai/assistant/index.php.
611 'baseUrl' => dol_buildpath('/ai/assistant/', 1),
612 'token' => newToken(),
613 'userInitial' => $userinitial,
614 );
615}
616
625function getAiChatAssistantHtml($mode = 'page')
626{
627 global $langs, $user, $conf;
628
629 $out = '';
630
631 // Config travels as a data attribute: <script> tags injected via innerHTML
632 // are never executed by the browser, so a window.AI_CONFIG inline script
633 // would not work for the AJAX-loaded popover.
634 $out .= '<div class="ai-chat-container'.($mode === 'popover' ? ' ai-in-popover' : '').'"';
635 $out .= ' data-ai-config="'.dol_escape_htmltag(json_encode(getAiChatAssistantConfig())).'"';
636 if ($mode === 'page') {
637 $out .= ' data-ai-autoinit="1"';
638 }
639 $out .= '>';
640
641 // Header
642 $out .= '<div class="chat-header">';
643 if ($mode === 'popover') {
644 // In the popover the title links to the full standalone page
645 $title = img_picto('', 'fa-robot', '', 0, 0, 0, '', 'paddingright').$langs->trans("AIAssistant");
646 $title = '<a href="'.dol_buildpath('/ai/assistant/index.php', 1).'" class="ai-header-link" title="'.dol_escape_htmltag($langs->trans("AIOpenFullPage")).'">'.$title.'</a>';
647 $out .= '<h2>'.$title.'</h2>';
648 } else {
649 // Full page: assistant identity (avatar + title + current LLM model)
650 $out .= '<div class="chat-header-id">';
651 $out .= '<span class="chat-header-avatar">'.img_picto('', 'fa-robot').'</span>';
652 $out .= '<span class="chat-header-text">';
653 $out .= '<span class="chat-header-title">'.$langs->trans("AIAssistant").'</span>';
654 $aiprovider = getAiAssistantProviderLabel();
655 if ($aiprovider !== '') {
656 $out .= '<span class="chat-header-status" title="'.dol_escape_htmltag($langs->trans("AIProviderInUse")).'">'.dol_escape_htmltag($aiprovider).'</span>';
657 }
658 $out .= '</span>';
659 $out .= '</div>';
660 }
661 $out .= '<div class="header-controls">';
662 // Model picker pill: 'Auto' (provider default) + presets + the dynamic model
663 // list fetched from ajax/list_models.php by the JS. Choice kept in localStorage.
664 $out .= '<select id="model-select" class="engine-select model-select" title="'.dol_escape_htmltag($langs->trans("AIModelToUse")).'">';
665 $out .= '<option value="">'.$langs->transnoentitiesnoconv("AIModelAuto").'</option>';
666 $out .= '</select>';
667 // Engine Switcher (restyled as a pill with a sparkle icon)
668 $out .= '<select id="engine-select" class="engine-select">';
669 $out .= '<option value="text">'.$langs->transnoentitiesnoconv("OptionTextOnly").'</option>';
670 $out .= '<option value="cloud">'.$langs->transnoentitiesnoconv("OptionCloudFast").'</option>';
671 $out .= '<option value="whisper">'.$langs->transnoentitiesnoconv("OptionWhisperLocal").'</option>';
672 // Note: the legacy 'local_docs'/'cloud_docs' selector modes are gone — documents
673 // are now attached with the always-visible paperclip button and routed
674 // automatically (local extraction first, cloud parsing as fallback).
675 $out .= '</select>';
676 // Clear Button
677 $out .= '<button type="button" id="clear-btn" class="icon-btn" title="'.dol_escape_htmltag($langs->trans("ClearChatHistoryTitle")).'">';
678 $out .= img_picto('', 'fa-trash').' <span class="ai-btn-label">'.$langs->trans("Clear").'</span>';
679 $out .= '</button>';
680 if ($mode === 'popover') {
681 // Window controls of the popover (handled by the bootstrap JS in main.inc.php)
682 $out .= '<button type="button" id="ai-expand-btn" class="icon-btn ai-window-btn" title="'.dol_escape_htmltag($langs->trans("AIExpandPanel")).'" data-title-expand="'.dol_escape_htmltag($langs->trans("AIExpandPanel")).'" data-title-reduce="'.dol_escape_htmltag($langs->trans("AIReducePanel")).'"><i class="fa fa-expand-alt"></i></button>';
683 $out .= '<button type="button" id="ai-close-btn" class="icon-btn ai-window-btn" title="'.dol_escape_htmltag($langs->trans("Close")).'"><i class="fa fa-times"></i></button>';
684 }
685 $out .= '</div>';
686 $out .= '</div>';
687
688 // Chat History
689 $out .= '<div id="chat-history" class="chat-history">';
690 if ($mode === 'popover') {
691 // Compact greeting line for the narrow popover
692 $out .= '<div class="msg system">'.$langs->trans("AIWelcomeMessage").'</div>';
693 } else {
694 // Full page: rich empty-state welcome screen (hidden by JS as soon as the
695 // conversation starts, restored on Clear). Quick cards send a localized
696 // ready-made prompt on click (data-prompt).
697 $welcomename = $user->firstname ? $user->firstname : (is_object($user) ? $user->login : '');
698 $quickcards = array(
699 array('icon' => 'fa-file-invoice-dollar', 'key' => 'Invoices'),
700 array('icon' => 'fa-chart-line', 'key' => 'Revenue'),
701 array('icon' => 'fa-coins', 'key' => 'Finance'),
702 array('icon' => 'fa-warehouse', 'key' => 'Inventory'),
703 );
704 $out .= '<div class="chat-welcome">';
705 $out .= '<div class="chat-welcome-avatar">'.img_picto('', 'fa-robot').'</div>';
706 $out .= '<h2 class="chat-welcome-title">'.dol_escape_htmltag($langs->trans("AIGreeting", $welcomename)).'</h2>';
707 $out .= '<p class="chat-welcome-subtitle">'.dol_escape_htmltag($langs->trans("AIGreetingSubtitle")).'</p>';
708 $out .= '<div class="chat-welcome-actions">';
709 foreach ($quickcards as $card) {
710 $out .= '<button type="button" class="ai-quick-card" data-prompt="'.dol_escape_htmltag($langs->transnoentitiesnoconv("AIQuick".$card['key']."Prompt")).'">';
711 $out .= '<span class="ai-quick-icon">'.img_picto('', $card['icon']).'</span>';
712 $out .= '<span class="ai-quick-text">';
713 $out .= '<span class="ai-quick-title">'.dol_escape_htmltag($langs->trans("AIQuick".$card['key']."Title")).'</span>';
714 $out .= '<span class="ai-quick-desc">'.dol_escape_htmltag($langs->trans("AIQuick".$card['key']."Desc")).'</span>';
715 $out .= '</span>';
716 $out .= '</button>';
717 }
718 $out .= '</div>';
719 $out .= '</div>';
720 }
721 $out .= '</div>';
722
723 // Controls: a single rounded "pill" holding the attach/mic buttons, the
724 // textarea and the send button.
725 $out .= '<div class="chat-controls">';
726 // Attached-file chip (icon + name + remove cross), shown above the input pill
727 // once a document has been attached with the paperclip. The document content
728 // itself NEVER appears in the input nor in the conversation.
729 $out .= '<div id="file-chip-area" class="file-chip-area ai-hidden"></div>';
730 $out .= '<div class="chat-input-pill">';
731 // Upload Wrapper (always visible: documents can be attached in any mode)
732 $out .= '<div id="upload-wrapper" class="upload-wrapper">';
733 $out .= '<input type="file" id="file-upload" accept=".pdf,.txt,.xml,.png,.jpg,.jpeg,.doc,.docx,.xls,.xlsx,.odt,.ods" style="display: none;">';
734 $out .= '<button type="button" id="upload-btn" class="round-btn" title="'.dol_escape_htmltag($langs->transnoentitiesnoconv("AttachFile")).'">'.img_picto('', 'fa-paperclip').'</button>';
735 $out .= '</div>';
736 // Microphone Wrapper (Visible only in Voice modes)
737 $out .= '<div id="mic-wrapper" class="mic-wrapper ai-hidden">';
738 $out .= '<button type="button" id="mic-btn" class="round-btn mic-btn" title="'.dol_escape_htmltag($langs->trans("ToggleMicrophone")).'">'.img_picto('', 'fa-microphone').'</button>';
739 $out .= '</div>';
740 // Text Input
741 $out .= '<textarea id="user-input" class="ia-input" rows="1"';
742 if (empty($conf->dol_optimize_smallscreen)) {
743 $out .= ' placeholder="'.dol_escape_htmltag($langs->trans("TypeYourQuestion")).'"';
744 }
745 $out .= ' autocomplete="off" spellcheck="false"></textarea>';
746 // Send Button
747 $out .= '<button type="button" id="send-btn" class="chat-send-btn" title="'.dol_escape_htmltag($langs->trans("SendPrompt")).'">'.img_picto('', 'fa-paper-plane').'</button>';
748 $out .= '</div>';
749 $out .= '</div>';
750
751 $out .= '<div id="status-bar"></div>';
752
753 $out .= '</div>';
754
755 return $out;
756}
testAIConnection(string $service, string $key, string $url)
Tests the connection to an AI service using its API key and URL by sending message "Hello".
Definition ai.lib.php:213
getListOfAIFeatures()
Prepare admin pages header.
Definition ai.lib.php:37
aiAdminPrepareHead()
Prepare admin pages header.
Definition ai.lib.php:408
getListOfAIServices()
Get list of available ai services.
Definition ai.lib.php:69
ai_log_request($db, $user, $query, array $response, $provider, float $time, float $confidence, $status, $error='', $rawReq='', $rawRes='')
Log AI Request with Raw Payloads.
Definition ai.lib.php:320
getAiChatAssistantConfig()
Build the configuration array consumed by the AI Assistant chat frontend (ai/js/ai_assistant....
Definition ai.lib.php:496
getListForAIRephraseStyle()
Get list for AI style of writing.
Definition ai.lib.php:392
getAiAssistantProviderLabel()
Resolve the AI provider/service currently configured for the AI Assistant (e.g.
Definition ai.lib.php:478
getListForAISummarize()
Get list for AI summarize.
Definition ai.lib.php:370
getAiChatAssistantHtml($mode='page')
Build the HTML of the AI Assistant chat interface.
Definition ai.lib.php:625
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dol_now($mode='gmt')
Return date for now.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
newToken()
Return the value of token currently saved into session with name 'newtoken'.
dol_strtoupper($string, $encoding="UTF-8")
Convert a string to upper.
dol_substr($string, $start, $length=null, $stringencoding='', $trunconbytes=0)
Make a substring.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
complete_head_from_modules($conf, $langs, $object, &$head, &$h, $type, $mode='add', $filterorigmodule='')
Complete or removed entries into a head array (used to build tabs).
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
Definition html.lib.php:172
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string