<?php
/* Copyright (C) 2022		Alice Adminson			<aadminson@example.com>
 * Copyright (C) 2024-2025  Frédéric France			<frederic.france@free.fr>
 * Copyright (C) 2024		MDW						<mdeweerd@users.noreply.github.com>
 * Copyright (C) 2026		Anthony Damhet			<a.damhet@progiseize.fr>
 * Copyright (C) 2026		Nick Fragoulis
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY, without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

/**
 * \file    htdocs/ai/lib/ai.lib.php
 * \ingroup ai
 * \brief   Library files with common functions for Ai
 */

include_once DOL_DOCUMENT_ROOT.'/ai/class/ai.class.php';


/**
 * Prepare admin pages header
 *
 * @return array<string,array<string,string>>
 */
function getListOfAIFeatures()
{
	global $langs;

	$arrayofaifeatures = array(
		'textgenerationemail' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("EmailContent").')', 'picto' => '', 'status' => 'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_EMAIL),
		'textgenerationwebpage' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("WebsitePage").')', 'picto' => '', 'status' => 'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_WEBPAGE),
		'textgeneration' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("Other").')', 'picto' => '', 'status' => 'notused', 'function' => 'TEXT'),

		'texttranslation' => array('label' => $langs->trans('TextTranslation'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION),
		'textsummarize' => array('label' => $langs->trans('TextSummarize'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE),
		'textspellchecker' => array('label' => $langs->trans('TextSpellChecker'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_SPELLCHECKER),
		'textrephrase' => array('label' => $langs->trans('TextRephraser'), 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER),

		'textgenerationextrafield' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("ExtrafieldFiller").')', 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER),

		'imagegeneration' => array('label' => 'ImageGeneration', 'picto' => '', 'status' => 'notused', 'function' => 'IMAGE'),
		'videogeneration' => array('label' => 'VideoGeneration', 'picto' => '', 'status' => 'notused', 'function' => 'VIDEO'),
		'audiogeneration' => array('label' => 'AudioGeneration', 'picto' => '', 'status' => 'notused', 'function' => 'AUDIO'),
		'transcription' => array('label' => 'AudioTranscription', 'picto' => '', 'status' => 'notused', 'function' => 'TRANSCRIPT'),
		'translation' => array('label' => 'AudioTranslation', 'picto' => '', 'status' => 'notused', 'function' => 'TRANSLATE'),
		'docparsing' => array('label' => 'DocumentParsing', 'picto' => '', 'status' => 'experimental', 'function' => 'DOCPARSING')
	);

	return $arrayofaifeatures;
}

/**
 * Get list of available ai services
 *
 * @return array<int|string,mixed>
 */
function getListOfAIServices()
{
	global $langs;

	$arrayofai = array(
		'-1' => array('label' => $langs->trans('SelectAService')),
		'chatgpt' => array(
			'label'           => 'ChatGPT (OpenAI)',
			'url'             => 'https://api.openai.com/v1/',
			'setup'           => 'https://platform.openai.com/account/api-keys',
			'textgeneration'  => array('default' => 'gpt-5.2'),             // Flagship model released late 2025, updated Feb 2026
			'imagegeneration' => array('default' => 'gpt-image-1.5'),       // Replaced DALL-E 3; 4x faster and native to GPT-5
			'audiogeneration' => array('default' => 'gpt-audio-1.5'),       // New Feb 23, 2026 release for high-fidelity audio out
			'videogeneration' => array('default' => 'sora-2'),              // OpenAI's standard API video model
			'transcription'   => array('default' => 'whisper-large-v3-turbo'), // The current speed/accuracy benchmark for ASR
			'translation'     => array('default' => 'whisper-large-v3-turbo'), // Still the best for multi-language audio translation
			'docparsing'      => array('default' => 'gpt-5.2'),             // Uses the new Responses API / Vision capabilities
			'adapter_type'    => 'openai'
		),
		'groq' => array(
			'label'           => 'Groq (LPU Inference)',
			'url'             => 'https://api.groq.com/openai/v1/',
			'setup'           => 'https://console.groq.com/keys',
			'textgeneration'  => array('default' => 'llama-4-8b-instant'),    // February 2026 flagship for extreme speed (1,000+ t/s)
			'imagegeneration' => array('default' => 'na'),
			'audiogeneration' => array('default' => 'na'),
			'videogeneration' => array('default' => 'na'),
			'transcription'   => array('default' => 'whisper-large-v3-turbo'), // Groq's specialized high-speed Whisper implementation
			'translation'     => array('default' => 'whisper-large-v3-turbo'), // High-speed audio translation to English
			'docparsing'      => array('default' => 'llama-4-70b-versatile'),  // Best for structured data extraction from text
			'adapter_type'    => 'openai'
		),
		'mistral' => array(
			'label' => 'Mistral AI',
			'url' => 'https://api.mistral.ai/v1/',
			'setup' => 'https://console.mistral.ai/api-keys/',
			'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)
			'imagegeneration' => array('default' => 'na'),
			'audiogeneration' => array('default' => 'na'),
			'videogeneration' => array('default' => 'na'),
			'transcription' => array('default' => 'na'),
			'translation' => array('default' => 'na'),
			'docparsing' => array('default' => 'pixtral-12b-latest'),         // Mistral's native vision/doc model
			'adapter_type' => 'openai'
		),
		'deepseek' => array(
			'label' => 'DeepSeek',
			'url' => 'https://api.deepseek.com',
			'setup' => 'https://platform.deepseek.com/api_keys',
			'textgeneration' => array('default' => 'deepseek-v4'),             // Released Feb 2026, flagship MoE model
			'imagegeneration' => array('default' => 'deepseek-janus-2'),       // DeepSeek's latest multimodal vision/gen model
			'audiogeneration' => array('default' => 'na'),
			'videogeneration' => array('default' => 'na'),
			'transcription' => array('default' => 'na'),
			'translation' => array('default' => 'na'),
			'docparsing' => array('default' => 'deepseek-v4'),                 // Massive 1M context support for parsing
			'adapter_type' => 'openai'
		),
		'perplexity' => array(
			'label' => 'Perplexity (Sonar)',
			'url' => 'https://api.perplexity.ai',
			'setup' => 'https://www.perplexity.ai/settings/api',
			'textgeneration' => array('default' => 'sonar-pro'),               // Flagship search model as of Feb 2026
			'imagegeneration' => array('default' => 'na'),
			'audiogeneration' => array('default' => 'na'),
			'videogeneration' => array('default' => 'na'),
			'transcription' => array('default' => 'na'),
			'translation' => array('default' => 'na'),
			'docparsing' => array('default' => 'sonar-reasoning'),             // Best for analyzing search-grounded docs
			'adapter_type' => 'openai'
		),
		'zai' => array(
			'label' => 'Zhipu AI (GLM)',
			'url' => 'https://api.z.ai/api/paas/v4',
			'setup' => 'https://docs.z.ai/guides/overview/quick-start',
			'textgeneration' => array('default' => 'glm-5'),                  // Flagship released February 11, 2026
			'imagegeneration' => array('default' => 'cogview-4'),              // Zhipu's latest SOTA image generator
			'audiogeneration' => array('default' => 'cogvlm2-audio'),          // High-fidelity conversational audio
			'videogeneration' => array('default' => 'cogvideox-2'),            // Flagship API video model
			'transcription' => array('default' => 'na'),
			'translation' => array('default' => 'na'),
			'docparsing' => array('default' => 'glm-5'),                      // Top-tier agentic document processing
			'adapter_type' => 'openai'
		),
		'custom' => array(
			'label' => 'Custom',
			'url' => 'https://domainofapi.com/v1/',
			'setup' => 'Ask your AI provider how to get your API key',
			'textgeneration' => array('default' => 'tinyllama-1.1b'),
			'imagegeneration' => array('default' => 'mixtral-8x7b-32768'),
			'audiogeneration' => array('default' => 'mixtral-8x7b-32768'),
			'videogeneration' => array('default' => 'na'),
			'transcription' => array('default' => 'mixtral-8x7b-32768'),
			'translation' => array('default' => 'mixtral-8x7b-32768'),
			'docparsing' => array('default' => 'na'),
			'adapter_type' => 'openai'
		),
		// --- SPECIALIZED ADAPTERS ---
		'anthropic' => array(
			'label' => 'Anthropic (Claude)',
			'url' => 'https://api.anthropic.com/v1/',
			'setup' => 'https://console.anthropic.com/',
			'textgeneration' => array('default' => 'claude-opus-4-6'),    // Released Feb 2026; features a 1M context window
			'imagegeneration' => array('default' => 'na'),              // Anthropic remains focused on text/code logic
			'audiogeneration' => array('default' => 'na'),
			'videogeneration' => array('default' => 'na'),
			'transcription' => array('default' => 'na'),
			'translation' => array('default' => 'na'),
			'docparsing' => array('default' => 'claude-opus-4-6'),      // Leading model for "Computer Use" and PDF analysis
			'adapter_type' => 'anthropic'
		),
		'google' => array(
			'label' => 'Google Gemini',
			'url' => 'https://generativelanguage.googleapis.com/v1beta/',
			'setup' => 'https://aistudio.google.com/',
			'textgeneration' => array('default' => 'gemini-3.1-pro-preview'), // Flagship reasoning model released Feb 19, 2026
			'imagegeneration' => array('default' => 'nano-banana-pro'),       // Latest SOTA image model (Gemini 3 Pro Image)
			'audiogeneration' => array('default' => 'gemini-2.5-pro-tts'),    // High-fidelity native speech synthesis
			'videogeneration' => array('default' => 'veo-3.1'),              // Google's flagship cinematic video API
			'transcription' => array('default' => 'gemini-3.1-pro-preview'),  // Native multi-modal audio reasoning
			'translation' => array('default' => 'gemini-3.1-pro-preview'),    // Native audio-to-text translation
			'docparsing' => array('default' => 'gemini-3.1-pro-preview'),     // Massive 2M+ context window for full repo parsing
			'adapter_type' => 'google'
		)
	);

	return $arrayofai;
}

/**
 * Tests the connection to an AI service using its API key and URL by sending message "Hello"
 *
 * This function supports multiple AI providers (Google Gemini, Anthropic Claude, and OpenAI-compatible APIs like
 * Mistral, Groq, and DeepSeek). It constructs a minimal, provider-specific request payload and sends it
 * to the given endpoint to verify that the API key is valid and the service is reachable.
 *
 * @param string $service The identifier of the AI service (e.g., 'google', 'anthropic', 'openai', 'mistral').
 * @param string $key The API key for the service.
 * @param string $url The base URL of the AI service's API endpoint.
 *
 * @return array{success: bool, message: string} An associative array indicating the result of the test.
 *               - 'success' is true on a successful connection (HTTP 2xx), false otherwise.
 *               - 'message' provides details, such as "OK (HTTP 200)" or an error description.
 */
function testAIConnection(string $service, string $key, string $url): array
{
	if (empty($key)) {
		return ['success' => false, 'message' => 'API Key is empty'];
	}

	// Load Defaults (Ensure this function exists or handle the error)
	if (!function_exists('getListOfAIServices')) {
		return ['success' => false, 'message' => 'Configuration helper function missing.'];
	}

	$list = getListOfAIServices();
	$defUrl = $list[$service]['url'] ?? '';
	// Use model from config, fallback to hardcoded if necessary
	$defaultModel = $list[$service]['model'] ?? 'unknown';

	// Normalize URL
	if (empty($url)) {
		$url = $defUrl;
	}
	$url = rtrim($url, '/');

	$data = [];
	$headers = ["Content-Type: application/json"];

	$model = '';
	if (empty($model)) {
		$model = getDolGlobalString('AI_API_' . strtoupper($service) . '_MODEL_TEXT');
	}

	// GOOGLE
	if ($service == 'google' || strpos($url, 'googleapis') !== false) {
		if (strpos($url, ':generateContent') === false) {
			if (strpos($url, 'models') === false) {
				$url .= "/models/$model:generateContent";
			} else {
				$url .= "/$model:generateContent";
			}
		}
		$url .= "?key=" . $key;
		$data = ["contents" => [ ["parts" => [ ["text" => "Hello"] ] ] ], "generationConfig" => ["maxOutputTokens" => 5]];
	} elseif ($service == 'anthropic' || strpos($url, 'anthropic') !== false) {  // ANTHROPIC
		if (strpos($url, 'messages') === false) $url .= '/messages';
		$headers[] = "x-api-key: $key";
		$headers[] = "anthropic-version: 2023-06-01";
		$data = [
			"model" => $model, // Uses Configured Model
			"messages" => [["role" => "user", "content" => "Hello"]],
			"max_tokens" => 5
		];
	} else {
		if (strpos($url, '/chat/completions') === false) $url .= '/chat/completions';
		$headers[] = "Authorization: Bearer $key";

		$data = [
			"model" => $model, // Uses Configured Model (from Priority Chain)
			"messages" => [["role" => "user", "content" => "Hello"]],
			"max_tokens" => 5
		];
	}

	// Execute cURL
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_POST, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt($ch, CURLOPT_TIMEOUT, 10);
	// Optional: Add SSL verification if behind a proxy with self-signed certs
	// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

	$result = curl_exec($ch);
	$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
	$err = curl_error($ch);
	curl_close($ch);

	if ($err) {
		return ['success' => false, 'message' => "Curl Error: $err"];
	}

	if ($httpCode >= 200 && $httpCode < 300) {
		return ['success' => true, 'message' => "OK (HTTP $httpCode)."];
	} else {
		$json = json_decode($result, true);
		// Attempt to find the error message in various common structures
		$msg = $json['error']['message'] ?? $json['message'] ?? substr($result, 0, 150);
		return ['success' => false, 'message' => "HTTP $httpCode. Error: $msg"];
	}
}

/**
 * Log AI Request with Raw Payloads
 *
 * @param   DoliDB                  $db         Database object
 * @param   User                    $user       User object
 * @param   string                  $query      The query sent to the AI
 * @param   array<string, mixed>    $response   The full response from the AI
 * @param   string                  $provider   The AI provider (e.g., 'OpenAI', 'Anthropic')
 * @param   float                   $time       Execution time in seconds
 * @param   float                   $confidence Confidence score from the AI (if any)
 * @param   string                  $status     Status of the request (e.g., 'success', 'error')
 * @param   string                  $error      Error message, if any
 * @param   string                  $rawReq     Raw request payload
 * @param   string                  $rawRes     Raw response payload
 * @return  int									Return 0
 */
function ai_log_request($db, $user, $query, array $response, $provider, float $time, float $confidence, $status, $error = '', $rawReq = '', $rawRes = '')
{
	global $conf;

	if (!getDolGlobalInt('AI_LOG_REQUESTS')) {
		return 0;
	}

	$tool = isset($response['tool']) ? (string) $response['tool'] : '';

	if (dol_strlen($rawReq) > 60000) {
		$rawReq = dol_substr($rawReq, 0, 60000) . '... [Truncated]';
	}

	$rawResStr = (string) $rawRes;
	if (dol_strlen($rawResStr) > 60000) {
		$rawResStr = dol_substr($rawResStr, 0, 60000) . '... [Truncated]';
	}

	$sql = "INSERT INTO " . MAIN_DB_PREFIX . "ai_request_log (";
	$sql .= "entity, date_request, fk_user, query_text, tool_name, provider, ";
	$sql .= "execution_time, confidence, status, error_msg, raw_request_payload, raw_response_payload";
	$sql .= ") VALUES (";
	$sql .= ((int) $conf->entity) . ", ";
	$sql .= "'" . $db->idate(dol_now()) . "', ";
	$sql .= ((int) $user->id) . ", ";
	$sql .= "'" . $db->escape($query) . "', ";
	$sql .= "'" . $db->escape($tool) . "', ";
	$sql .= "'" . $db->escape($provider) . "', ";
	$sql .= ((float) $time) . ", ";
	$sql .= ((float) $confidence) . ", ";
	$sql .= "'" . $db->escape($status) . "', ";
	$sql .= "'" . $db->escape($error) . "', ";
	$sql .= "'" . $db->escape($rawReq) . "', ";
	$sql .= "'" . $db->escape($rawResStr) . "'";
	$sql .= ")";

	$resql = $db->query($sql);
	if (!$resql) {
		dol_print_error($db);
	}

	return 0;
}

/**
 * Get list for AI summarize
 *
 * @return array<int|string,mixed>
 */
function getListForAISummarize()
{
	$arrayforaisummarize = array(
		//'20_w' => 'SummarizeTwentyWords',
		'50_w' => 'SummarizeFiftyWords',
		'100_w' => 'SummarizeHundredWords',
		'200_w' => 'SummarizeTwoHundredWords',
		'1_p' => 'SummarizeOneParagraphs',
		'2_p' => 'SummarizeTwoParagraphs',
		'25_pc' => 'SummarizeTwentyFivePercent',
		'50_pc' => 'SummarizeFiftyPercent',
		'75_pc' => 'SummarizeSeventyFivePercent'
	);

	return $arrayforaisummarize;
}

/**
 * Get list for AI style of writing
 *
 * @return array<int|string,mixed>
 */
function getListForAIRephraseStyle()
{
	$arrayforaierephrasestyle = array(
		'spellchecker' => 'RephraseSpellChecker',
		'professional' => 'RephraseStyleProfessional',
		'humouristic' => 'RephraseStyleHumouristic',
	);

	return $arrayforaierephrasestyle;
}

/**
 * Prepare admin pages header
 *
 * @return array<array{0:string,1:string,2:string}>
 */
function aiAdminPrepareHead()
{
	global $langs, $conf;

	$langs->load("agenda");

	$h = 0;
	$head = array();

	$head[$h][0] = dol_buildpath("/ai/admin/setup.php", 1);
	$head[$h][1] = $langs->trans("Settings");
	$head[$h][2] = 'settings';
	$h++;

	$head[$h][0] = dol_buildpath("/ai/admin/custom_prompt.php", 1);
	$head[$h][1] = $langs->trans("CustomPrompt");
	$head[$h][2] = 'custom';
	$h++;

	if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 2) {
		$head[$h][0] = dol_buildpath("/ai/admin/assistant.php", 1);
		$head[$h][1] = $langs->trans("Assistant");
		$head[$h][2] = 'assistant';
		$h++;
	}

	if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 2) {
		$head[$h][0] = dol_buildpath("/ai/admin/server_mcp.php", 1);
		$head[$h][1] = $langs->trans("MCPServer");
		$head[$h][2] = 'servermcp';
		$h++;
	}

	if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 2) {
		$head[$h][0] = dol_buildpath("/ai/admin/configure_tools.php", 1);
		$head[$h][1] = $langs->trans("ToolAccessControl");
		$head[$h][2] = 'tools';
		$h++;
	}

	/*
	$head[$h][0] = dol_buildpath("/ai/admin/myobject_extrafields.php", 1);
	$head[$h][1] = $langs->trans("ExtraFields");
	$head[$h][2] = 'myobject_extrafields';
	$h++;
	*/

	// Show more tabs from modules
	// Entries must be declared in modules descriptor with line
	//$this->tabs = array(
	//	'entity:+tabname:Title:@ai:/ai/mypage.php?id=__ID__'
	//); // to add new tab
	//$this->tabs = array(
	//	'entity:-tabname:Title:@ai:/ai/mypage.php?id=__ID__'
	//); // to remove a tab
	complete_head_from_modules($conf, $langs, null, $head, $h, 'ai@ai');

	complete_head_from_modules($conf, $langs, null, $head, $h, 'ai@ai', 'remove');

	return $head;
}

/**
 * Resolve the AI provider/service currently configured for the AI Assistant
 * (e.g. "ChatGPT (OpenAI)", "Google Gemini", "Anthropic (Claude)"), so it can be
 * displayed in the chat header. The precise model name is intentionally not
 * shown here, only which AI is in use.
 *
 * @return string	The provider label, or '' if no service is configured
 */
function getAiAssistantProviderLabel()
{
	$serviceKey = getDolGlobalString('AI_API_SERVICE');
	if (empty($serviceKey) || $serviceKey === '-1') {
		return '';
	}

	$services = getListOfAIServices();

	return isset($services[$serviceKey]['label']) ? (string) $services[$serviceKey]['label'] : (string) $serviceKey;
}

/**
 * Build the configuration array consumed by the AI Assistant chat frontend (ai/js/ai_assistant.js).
 * It is serialized as JSON into the data-ai-config attribute of the chat container.
 *
 * @return array{mode:string,labels:array<string,string>,baseUrl:string,token:string,userInitial:string}
 */
function getAiChatAssistantConfig()
{
	global $langs, $user;

	$keys = array(
		// General UI
		'NoDataAvailable',
		'Error',
		'NoRecordFound',
		'Download',
		'Show',
		'Confirm',
		'ConfirmAiAction',
		'ClearChatHistoryTitle',
		'HistoryCleared',
		'Send',
		'TypeYourQuestion',

		// Placeholders & Status
		'TypeOrSpeak',
		'UploadLocalDoc',
		'UploadCloudDoc',
		'DocLoaded',
		'Listening',
		'Transcribed',
		'NoSpeech',
		'ProcessingAudio',
		'Timeout',
		'Cancelled',

		// Engine Specific
		'CloudSpeechReady',
		'WhisperReady',
		'DownloadingModel',
		'ModelLoading',

		// Document Processing
		'ProcessingFile',
		'ReadingPdf',
		'PdfError',
		'UnsupportedFileType',
		'TryingOCR',
		'OcrProgress',
		'SwitchingAIModel',
		'OcrFailed',
		'ReadingWord',
		'ReadingExcel',
		'ReadingOdf',

		// Errors
		'MicError',
		'MicTooQuiet',
		'ConnectionBlocked',
		'ConnectionBlockedHelp',
		'WorkerInitFailed',
		'NetworkError',
		'AIError',
		'EmptyAIResponse',
		'BrowserNotSupported',

		// Actions & Dialogs
		'YesProceed',
		'Cancel',
		'Submit',
		'ActionCancelled',
		'ExecutingTool',
		'FetchingData',
		'GeneratingLink',
		'Found',
		'TypeResponse',
		'OpenVerb',

		// Voice Confirmation
		'VoiceYesNo',
		'VoiceQuiet',
		'PleaseRepeat',
		'HeardText',

		// Context
		'DocContextIntro',
		'DocContextOutro'
	);

	$ai_translations = array();
	foreach ($keys as $key) {
		$ai_translations[$key] = $langs->transnoentitiesnoconv($key);
	}
	$ai_translations['DownloadPdf'] = $langs->transnoentitiesnoconv("Download").' PDF';
	$ai_translations['CloudVoiceRequiresSecureContext'] = $langs->trans(
		"CloudVoiceRequiresSecureContext",
		"HTTPS",
		"localhost",
		"Whisper"
	);

	// First letter of the current user name, shown in the "user" message avatar
	$userinitial = '';
	if (is_object($user)) {
		$namesource = $user->firstname ? $user->firstname : ($user->login ? $user->login : '');
		$userinitial = dol_strtoupper(dol_substr($namesource, 0, 1));
	}

	return array(
		'mode' => getDolGlobalString('AI_DEFAULT_INPUT_MODE'),
		'labels' => $ai_translations,
		// Endpoints are called with absolute URLs so the chat also works when
		// injected into another page (topbar popover) and not only when served
		// from /ai/assistant/index.php.
		'baseUrl' => dol_buildpath('/ai/assistant/', 1),
		'token' => newToken(),
		'userInitial' => $userinitial,
	);
}

/**
 * Build the HTML of the AI Assistant chat interface.
 * Shared by the standalone page (ai/assistant/index.php) and the topbar popover
 * fragment (ai/assistant/popover.php) so both render the exact same chat.
 *
 * @param	string	$mode	'page' for the standalone full page, 'popover' for the topbar popover fragment
 * @return	string			HTML content
 */
function getAiChatAssistantHtml($mode = 'page')
{
	global $langs, $user, $conf;

	$out = '';

	// Config travels as a data attribute: <script> tags injected via innerHTML
	// are never executed by the browser, so a window.AI_CONFIG inline script
	// would not work for the AJAX-loaded popover.
	$out .= '<div class="ai-chat-container'.($mode === 'popover' ? ' ai-in-popover' : '').'"';
	$out .= ' data-ai-config="'.dol_escape_htmltag(json_encode(getAiChatAssistantConfig())).'"';
	if ($mode === 'page') {
		$out .= ' data-ai-autoinit="1"';
	}
	$out .= '>';

	// Header
	$out .= '<div class="chat-header">';
	if ($mode === 'popover') {
		// In the popover the title links to the full standalone page
		$title = img_picto('', 'fa-robot', '', 0, 0, 0, '', 'paddingright').$langs->trans("AIAssistant");
		$title = '<a href="'.dol_buildpath('/ai/assistant/index.php', 1).'" class="ai-header-link" title="'.dol_escape_htmltag($langs->trans("AIOpenFullPage")).'">'.$title.'</a>';
		$out .= '<h2>'.$title.'</h2>';
	} else {
		// Full page: assistant identity (avatar + title + current LLM model)
		$out .= '<div class="chat-header-id">';
		$out .= '<span class="chat-header-avatar">'.img_picto('', 'fa-robot').'</span>';
		$out .= '<span class="chat-header-text">';
		$out .= '<span class="chat-header-title">'.$langs->trans("AIAssistant").'</span>';
		$aiprovider = getAiAssistantProviderLabel();
		if ($aiprovider !== '') {
			$out .= '<span class="chat-header-status" title="'.dol_escape_htmltag($langs->trans("AIProviderInUse")).'">'.dol_escape_htmltag($aiprovider).'</span>';
		}
		$out .= '</span>';
		$out .= '</div>';
	}
	$out .= '<div class="header-controls">';
	// Engine Switcher (restyled as a pill with a sparkle icon)
	$out .= '<select id="engine-select" class="engine-select">';
	$out .= '<option value="text">'.$langs->transnoentitiesnoconv("OptionTextOnly").'</option>';
	$out .= '<option value="cloud">'.$langs->transnoentitiesnoconv("OptionCloudFast").'</option>';
	$out .= '<option value="whisper">'.$langs->transnoentitiesnoconv("OptionWhisperLocal").'</option>';
	$out .= '<option value="local_docs">'.$langs->transnoentitiesnoconv("OptionLocalParsing").'</option>';
	$out .= '<option value="cloud_docs">'.$langs->transnoentitiesnoconv("OptionCloudParsing").'</option>';
	$out .= '</select>';
	// Clear Button
	$out .= '<button type="button" id="clear-btn" class="icon-btn" title="'.dol_escape_htmltag($langs->trans("ClearChatHistoryTitle")).'">';
	$out .= img_picto('', 'fa-trash').' <span class="ai-btn-label">'.$langs->trans("Clear").'</span>';
	$out .= '</button>';
	if ($mode === 'popover') {
		// Window controls of the popover (handled by the bootstrap JS in main.inc.php)
		$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>';
		$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>';
	}
	$out .= '</div>';
	$out .= '</div>';

	// Chat History
	$out .= '<div id="chat-history" class="chat-history">';
	if ($mode === 'popover') {
		// Compact greeting line for the narrow popover
		$out .= '<div class="msg system">'.$langs->trans("AIWelcomeMessage").'</div>';
	} else {
		// Full page: rich empty-state welcome screen (hidden by JS as soon as the
		// conversation starts, restored on Clear). Quick cards send a localized
		// ready-made prompt on click (data-prompt).
		$welcomename = $user->firstname ? $user->firstname : (is_object($user) ? $user->login : '');
		$quickcards = array(
			array('icon' => 'fa-file-invoice-dollar', 'key' => 'Invoices'),
			array('icon' => 'fa-chart-line', 'key' => 'Revenue'),
			array('icon' => 'fa-coins', 'key' => 'Finance'),
			array('icon' => 'fa-warehouse', 'key' => 'Inventory'),
		);
		$out .= '<div class="chat-welcome">';
		$out .= '<div class="chat-welcome-avatar">'.img_picto('', 'fa-robot').'</div>';
		$out .= '<h2 class="chat-welcome-title">'.dol_escape_htmltag($langs->trans("AIGreeting", $welcomename)).'</h2>';
		$out .= '<p class="chat-welcome-subtitle">'.dol_escape_htmltag($langs->trans("AIGreetingSubtitle")).'</p>';
		$out .= '<div class="chat-welcome-actions">';
		foreach ($quickcards as $card) {
			$out .= '<button type="button" class="ai-quick-card" data-prompt="'.dol_escape_htmltag($langs->transnoentitiesnoconv("AIQuick".$card['key']."Prompt")).'">';
			$out .= '<span class="ai-quick-icon">'.img_picto('', $card['icon']).'</span>';
			$out .= '<span class="ai-quick-text">';
			$out .= '<span class="ai-quick-title">'.dol_escape_htmltag($langs->trans("AIQuick".$card['key']."Title")).'</span>';
			$out .= '<span class="ai-quick-desc">'.dol_escape_htmltag($langs->trans("AIQuick".$card['key']."Desc")).'</span>';
			$out .= '</span>';
			$out .= '</button>';
		}
		$out .= '</div>';
		$out .= '</div>';
	}
	$out .= '</div>';

	// Controls: a single rounded "pill" holding the attach/mic buttons, the
	// textarea and the send button.
	$out .= '<div class="chat-controls">';
	$out .= '<div class="chat-input-pill">';
	// Upload Wrapper (Visible only in Doc modes)
	$out .= '<div id="upload-wrapper" class="upload-wrapper ai-hidden">';
	$out .= '<input type="file" id="file-upload" accept=".pdf,.txt,.xml,.png,.jpg,.jpeg,.doc,.docx,.xls,.xlsx,.odt,.ods" style="display: none;">';
	$out .= '<button type="button" id="upload-btn" class="round-btn" title="'.dol_escape_htmltag($langs->transnoentitiesnoconv("AttachFile")).'">'.img_picto('', 'fa-paperclip').'</button>';
	$out .= '</div>';
	// Microphone Wrapper (Visible only in Voice modes)
	$out .= '<div id="mic-wrapper" class="mic-wrapper ai-hidden">';
	$out .= '<button type="button" id="mic-btn" class="round-btn mic-btn" title="'.dol_escape_htmltag($langs->trans("ToggleMicrophone")).'">'.img_picto('', 'fa-microphone').'</button>';
	$out .= '</div>';
	// Text Input
	$out .= '<textarea id="user-input" class="ia-input" rows="1"';
	if (empty($conf->dol_optimize_smallscreen)) {
		$out .= ' placeholder="'.dol_escape_htmltag($langs->trans("TypeYourQuestion")).'"';
	}
	$out .= ' autocomplete="off" spellcheck="false"></textarea>';
	// Send Button
	$out .= '<button type="button" id="send-btn" class="chat-send-btn" title="'.dol_escape_htmltag($langs->trans("SendPrompt")).'">'.img_picto('', 'fa-paper-plane').'</button>';
	$out .= '</div>';
	$out .= '</div>';

	$out .= '<div id="status-bar"></div>';

	$out .= '</div>';

	return $out;
}
