26require
'../../main.inc.php';
35require_once DOL_DOCUMENT_ROOT .
'/core/lib/admin.lib.php';
36require_once DOL_DOCUMENT_ROOT .
'/core/lib/date.lib.php';
37require_once DOL_DOCUMENT_ROOT .
'/core/class/html.form.class.php';
40$langs->loadLangs(array(
"admin",
"other"));
43$action =
GETPOST(
'action',
'aZ09');
44$massaction =
GETPOST(
'massaction',
'alpha');
45$confirm =
GETPOST(
'confirm',
'alpha');
46$toselect =
GETPOST(
'toselect',
'array');
47$contextpage =
GETPOST(
'contextpage',
'aZ') ?
GETPOST(
'contextpage',
'aZ') :
'ailoglist';
48$optioncss =
GETPOST(
'optioncss',
'alpha');
49$mode =
GETPOST(
'mode',
'alpha');
54$search_user =
GETPOST(
'search_user',
'alpha');
55$search_query =
GETPOST(
'search_query',
'alpha');
56$search_tool =
GETPOST(
'search_tool',
'alpha');
57$search_provider =
GETPOST(
'search_provider',
'alpha');
58$search_time_min =
GETPOST(
'search_time_min',
'alpha');
59$search_time_max =
GETPOST(
'search_time_max',
'alpha');
60$search_status =
GETPOST(
'search_status',
'alpha');
64$sortfield =
GETPOST(
'sortfield',
'aZ09comma');
65$sortorder =
GETPOST(
'sortorder',
'aZ09comma');
67if (empty($page) || $page == -1) {
70$offset = $limit * $page;
71if (!$sortfield) $sortfield =
"l.date_request";
72if (!$sortorder) $sortorder =
"DESC";
76 'search_date_start' => $search_date_start,
77 'search_date_end' => $search_date_end,
78 'search_user' => $search_user,
79 'search_query' => $search_query,
80 'search_tool' => $search_tool,
81 'search_provider' => $search_provider,
82 'search_time_min' => $search_time_min,
83 'search_time_max' => $search_time_max,
84 'search_status' => $search_status
101if ($action ==
'purge' && $confirm ==
'yes') {
104 $sql =
"DELETE FROM " . MAIN_DB_PREFIX .
"ai_request_log";
105 $sql .=
" WHERE entity IN (" .
getEntity(
'airequestlog') .
")";
107 $resql = $db->query($sql);
110 $nbDeleted = $db->affected_rows($resql);
112 setEventMessages($langs->trans(
"LogsCleared") .
" (" . $nbDeleted .
")",
null,
'mesgs');
115 setEventMessages($db->lasterror(),
null,
'errors');
118 header(
'Location: ' . $_SERVER[
"PHP_SELF"]);
123if ($massaction ==
'purge' && !empty($toselect) && is_array($toselect)) {
126 foreach ($toselect as
$id) {
127 $sql =
"DELETE FROM " . MAIN_DB_PREFIX .
"ai_request_log";
128 $sql .=
" WHERE rowid = " . ((int)
$id);
129 $sql .=
" AND entity IN (" .
getEntity(
'airequestlog') .
")";
131 $resql = $db->query($sql);
135 setEventMessages($db->lasterror(),
null,
'errors');
142 setEventMessages($langs->trans(
"SelectedLogsDeleted"),
null,
'mesgs');
152if (
GETPOST(
'button_removefilter',
'alpha') ||
GETPOST(
'button_removefilter_x',
'alpha')) {
153 $search_date_start =
'';
154 $search_date_end =
'';
158 $search_provider =
'';
159 $search_time_min =
'';
160 $search_time_max =
'';
173if ($contextpage != $_SERVER[
"PHP_SELF"]) {
174 $param .=
'&contextpage='.urlencode($contextpage);
176if ($limit > 0 && $limit !=
$conf->liste_limit) {
177 $param .=
'&limit='.((int) $limit);
179foreach ($search_array as $key => $val) {
180 if (!empty($val) || $val ===
'0') {
181 $param .=
'&' . urlencode($key) .
'=' . urlencode($val);
185llxHeader(
'', $langs->trans(
"AIRequestLogs"),
'');
191$where[] =
"l.entity IN (" .
getEntity(
'airequestlog') .
")";
193if ($search_date_start) {
194 $where[] =
"l.date_request >= '" . $db->escape(
date(
'Y-m-d H:i:s', $search_date_start)) .
"'";
196if ($search_date_end) {
197 $where[] =
"l.date_request <= '" . $db->escape(
date(
'Y-m-d H:i:s', $search_date_end)) .
"'";
200 $where[] =
"u.login LIKE '%" . $db->escape($search_user) .
"%'";
203 $where[] =
"l.query_text LIKE '%" . $db->escape($search_query) .
"%'";
206 $where[] =
"l.tool_name LIKE '%" . $db->escape($search_tool) .
"%'";
208if ($search_provider) {
209 $where[] =
"l.provider LIKE '%" . $db->escape($search_provider) .
"%'";
211if ($search_time_min) {
212 $where[] =
"l.execution_time >= " . floatval($search_time_min);
214if ($search_time_max) {
215 $where[] =
"l.execution_time <= " . floatval($search_time_max);
218 $where[] =
"l.status = '" . $db->escape($search_status) .
"'";
223 $whereSQL =
' WHERE ' . implode(
' AND ', $where);
227$sqlCount =
"SELECT COUNT(l.rowid) as total
228 FROM " . MAIN_DB_PREFIX .
"ai_request_log as l
229 LEFT JOIN " . MAIN_DB_PREFIX .
"user as u ON l.fk_user = u.rowid";
230$sqlCount .= $whereSQL;
231$resqlCount = $db->query($sqlCount);
232$totalRecords = $resqlCount ? $db->fetch_object($resqlCount)->total : 0;
236$sqlSum =
"SELECT l.model, SUM(l.tokens_input) as tin, SUM(l.tokens_output) as tout
237 FROM " . MAIN_DB_PREFIX .
"ai_request_log as l
238 LEFT JOIN " . MAIN_DB_PREFIX .
"user as u ON l.fk_user = u.rowid";
240$sqlSum .=
" GROUP BY l.model";
241$resqlSum = $db->query($sqlSum);
254 static $prices =
null;
255 if ($prices ===
null) {
257 if (!is_array($prices)) {
261 $entry = $prices[$model] ?? ($prices[
'default'] ??
null);
262 return (is_array($entry) && isset($entry[
'in'], $entry[
'out'])) ? array(
'in' => (
float) $entry[
'in'],
'out' => (float) $entry[
'out']) :
null;
266$sql =
"SELECT l.rowid, u.login, l.date_request, l.query_text, l.tool_name, l.provider, l.execution_time, l.status, l.error_msg, l.raw_request_payload, l.raw_response_payload, l.tokens_input, l.tokens_output, l.model
267 FROM " . MAIN_DB_PREFIX .
"ai_request_log as l
268 LEFT JOIN " . MAIN_DB_PREFIX .
"user as u ON l.fk_user = u.rowid";
270$sql .= $db->order($sortfield, $sortorder);
271$sql .= $db->plimit($limit, $offset);
273$resql = $db->query($sql);
274$num = $db->num_rows($resql);
280$title = $langs->trans(
"AIRequestLogs");
281print_barre_liste($title, $page, $_SERVER[
"PHP_SELF"], $param, $sortfield, $sortorder,
'', $num, $totalRecords,
'title_ai', 0,
'',
'', $limit, 1, 0, 0,
'');
289 $costComplete =
true;
290 while ($sumrow = $db->fetch_object($resqlSum)) {
291 $totIn += (int) $sumrow->tin;
292 $totOut += (int) $sumrow->tout;
295 $totCost += ((int) $sumrow->tin * $price[
'in'] + (
int) $sumrow->tout * $price[
'out']) / 1000000;
296 } elseif ((
int) $sumrow->tin + (
int) $sumrow->tout > 0) {
297 $costComplete =
false;
300 if ($totIn + $totOut > 0) {
301 print
'<div class="opacitymedium paddingbottom">';
302 print $langs->trans(
"Total").
': '.number_format($totIn, 0,
'',
' ').
' in / '.number_format($totOut, 0,
'',
' ').
' out';
303 print
' — '.price($totCost, 0, $langs, 1, -1, 4).($costComplete ?
'' :
' (+ '.$langs->trans(
"Unknown").
')');
304 print
' <span title="AI_MODEL_PRICES_JSON, price per 1M tokens, e.g. {"gemini-2.5-flash":{"in":0.30,"out":2.50},"default":{"in":1,"out":3}}">'.img_info(
'').
'</span>';
309print
'<form method="POST" action="' . $_SERVER[
"PHP_SELF"] .
'" name="limitform">';
310print
'<input type="hidden" name="token" value="' .
newToken() .
'">';
311print
'<input type="hidden" name="action" value="list">';
314foreach ($search_array as $key => $val) {
315 if (!empty($val) || $val ===
'0') {
316 print
'<input type="hidden" name="' . $key .
'" value="' .
dol_escape_htmltag($val) .
'">';
320print
'<div class="div-table-responsive-no-min">';
321print
'<table class="noborder" width="100%">';
323print
'<td class="right">';
324print $langs->trans(
"Show") .
': ';
325print
'<input type="hidden" name="contextpage" value="'.$contextpage.
'">';
326print
'<input type="hidden" name="sortfield" value="'.$sortfield.
'">';
327print
'<input type="hidden" name="sortorder" value="'.$sortorder.
'">';
328print
'<input type="hidden" name="page" value="'.$page.
'">';
331 $arrayoflimit = array(5, 10, 20, 50, 100, 500, 1000);
332print
'<select class="flat" name="limit" onchange="this.form.submit()">';
333foreach ($arrayoflimit as $val) {
334 print
'<option value="'.$val.
'"';
335 if ($limit == $val) print
' selected';
336 print
'>'.$val.
'</option>';
340print
' ' . $langs->trans(
"Entries");
348print
'<form method="POST" action="' . $_SERVER[
"PHP_SELF"] .
'" name="search_form">';
349print
'<input type="hidden" name="token" value="' .
newToken() .
'">';
350print
'<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
351print
'<input type="hidden" name="sortfield" value="' . $sortfield .
'">';
352print
'<input type="hidden" name="sortorder" value="' . $sortorder .
'">';
353print
'<input type="hidden" name="page" value="' . $page .
'">';
354print
'<input type="hidden" name="contextpage" value="'.$contextpage.
'">';
355print
'<input type="hidden" name="page_y" value="">';
356print
'<input type="hidden" name="mode" value="'.$mode.
'">';
358print
'<div class="div-table-responsive">';
359print
'<table class="tagtable liste listwithfilterbefore">'.
"\n";
362print
'<tr class="liste_titre">';
363print_liste_field_titre(
"Date", $_SERVER[
"PHP_SELF"],
"l.date_request",
"", $param,
'', $sortfield, $sortorder);
366print_liste_field_titre(
"MCPTool", $_SERVER[
"PHP_SELF"],
"l.tool_name",
"", $param,
'', $sortfield, $sortorder);
367print_liste_field_titre(
"Provider", $_SERVER[
"PHP_SELF"],
"l.provider",
"", $param,
'', $sortfield, $sortorder);
368print_liste_field_titre(
"Time", $_SERVER[
"PHP_SELF"],
"l.execution_time",
"", $param,
'align="center"', $sortfield, $sortorder);
369print_liste_field_titre(
"Tokens", $_SERVER[
"PHP_SELF"],
"l.tokens_output",
"", $param,
'align="center" title="input / output (thinking included)"', $sortfield, $sortorder);
371print_liste_field_titre(
"Status", $_SERVER[
"PHP_SELF"],
"l.status",
"", $param,
'align="center"', $sortfield, $sortorder);
375print
'<tr class="liste_titre_filter">';
377print
'<td class="liste_titre">';
378print $form->selectDate($search_date_start,
'search_date_start', 0, 0, 1,
'', 1, 0, 0,
'',
'',
'',
'', 1,
'', $langs->trans(
"From"));
380print $form->selectDate($search_date_end,
'search_date_end', 0, 0, 1,
'', 1, 0, 0,
'',
'',
'',
'', 1,
'', $langs->trans(
"To"));
383print
'<td class="liste_titre"><input type="text" name="search_user" value="' .
dol_escape_htmltag($search_user) .
'" class="maxwidth100"></td>';
385print
'<td class="liste_titre"><input type="text" name="search_query" value="' .
dol_escape_htmltag($search_query) .
'" class="maxwidth150"></td>';
387print
'<td class="liste_titre"><input type="text" name="search_tool" value="' .
dol_escape_htmltag($search_tool) .
'" class="maxwidth100"></td>';
389print
'<td class="liste_titre"><input type="text" name="search_provider" value="' .
dol_escape_htmltag($search_provider) .
'" class="maxwidth100"></td>';
391print
'<td class="liste_titre center">';
395print
'<td class="liste_titre"></td>';
396print
'<td class="liste_titre"></td>';
398print
'<td class="liste_titre center">';
399$status_options = array(
'' => $langs->trans(
"All"),
'success' => $langs->trans(
"Success"),
'confirm' => $langs->trans(
"Confirm"),
'error' => $langs->trans(
"Error"));
400print $form->selectarray(
'search_status', $status_options, $search_status, 0, 0, 0,
'', 1);
403print
'<td class="liste_titre center">';
404$searchpicto = img_picto($langs->trans(
"Search"),
'search.png',
'', 0, 1);
405print
'<input type="image" class="liste_titre" name="button_search" src="' . $searchpicto .
'" value="' .
dol_escape_htmltag($langs->trans(
"Search")) .
'" title="' .
dol_escape_htmltag($langs->trans(
"Search")) .
'">';
406$clearpicto = img_picto($langs->trans(
"RemoveFilter"),
'searchclear.png',
'', 0, 1);
407print
'<input type="image" class="liste_titre" name="button_removefilter" src="' . $clearpicto .
'" value="' .
dol_escape_htmltag($langs->trans(
"RemoveFilter")) .
'" title="' .
dol_escape_htmltag($langs->trans(
"RemoveFilter")) .
'">';
412print
'<tr class="liste_titre">';
413print
'<td class="liste_titre" colspan="10">';
414print
'<div class="center">';
415print
'<div class="inline-block divButAction"><a class="butAction" href="'.$_SERVER[
"PHP_SELF"].
'?action=purge&token='.
newToken().
'" onclick="return confirm(\''.$langs->trans(
"ConfirmDeleteAllLogs").
'\');
">'.$langs->trans("ClearAllLogs
").'</a></div>';
420if ($resql && $num > 0) {
422 while ($obj = $db->fetch_object($resql)) {
423 print '<tr class="oddeven
">';
426 print '<td>' . dol_print_date($db->jdate($obj->date_request), 'dayhour') . '</td>';
429 print '<td>' . ($obj->login ? dol_escape_htmltag($obj->login) : $langs->trans("Unknown
")) . '</td>';
431 // Query - properly escaped
432 $shortQuery = dol_trunc($obj->query_text, 60);
433 print '<td title="' . dol_escape_htmltag($obj->query_text) . '">' . dol_escape_htmltag($shortQuery) . '</td>';
436 print '<td>' . dol_escape_htmltag($obj->tool_name) . '</td>';
439 print '<td>' . dol_escape_htmltag($obj->provider) . '</td>';
442 $timeColor = ($obj->execution_time > 5) ? 'color:red;' : '';
443 print '<td style="' . $timeColor . '" align="center
">' . round($obj->execution_time, 2) . 's</td>';
445 // Tokens (input / output) and cost, when the provider reported usage
446 if (isset($obj->tokens_input) || isset($obj->tokens_output)) {
447 print '<td align="center
" class="nowrap
" title="' . dol_escape_htmltag((string) $obj->model) . '">' . ((int) $obj->tokens_input) . ' / ' . ((int) $obj->tokens_output) . '</td>';
448 $price = aiModelPrice((string) $obj->model);
450 $cost = ((int) $obj->tokens_input * $price['in'] + (int) $obj->tokens_output * $price['out']) / 1000000;
451 print '<td align="center
" class="nowrap
">' . price($cost, 0, $langs, 1, -1, 4) . '</td>';
453 print '<td align="center
" class="opacitymedium
">-</td>';
456 print '<td align="center
" class="opacitymedium
">-</td><td align="center
" class="opacitymedium
">-</td>';
460 $badge = 'badge-status0';
461 if ($obj->status == $langs->transnoentitiesnoconv("Success
")) {
462 $badge = 'badge-status4'; // Green
464 if ($obj->status == $langs->transnoentitiesnoconv("Confirm
")) {
465 $badge = 'badge-status3'; // Yellow
467 if ($obj->status == $langs->transnoentitiesnoconv('Error')) {
468 $badge = 'badge-status8'; // Red
470 print '<td align="center
"><span class="badge
' . $badge . '">' . dol_escape_htmltag($obj->status) . '</span></td>';
472 // Details Button (Triggers Modal)
473 // We embed data attributes securely with proper UTF-8 handling
474 $reqSafe = base64_encode($obj->raw_request_payload);
475 $resSafe = base64_encode($obj->raw_response_payload);
476 $errSafe = base64_encode($obj->error_msg);
478 print '<td align="center
">';
479 print '<a href="#
" class="button button-small
" onclick="openLogModal(
this)
"
480 data-req="' . dol_escape_htmltag($reqSafe) . '"
481 data-res="' . dol_escape_htmltag($resSafe) . '"
482 data-err="' . dol_escape_htmltag($errSafe) . '">';
483 print '<span class="fa fa-search-plus
"></span> ' . $langs->trans("View
");
492 print '<tr><td colspan="' . $colspan . '" class="opacitymedium
">' . $langs->trans("NoLogsFound
");
493 if (!empty($where)) {
494 print ' ' . $langs->trans("MatchingSearchCriteria
");
496 print '. ' . $langs->trans("TryAskingAI
") . '.</td></tr>';
498print '</table></div>';
502// --- MODAL HTML & JS ---
504<div id="logModal
" style="display:none;
position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; overflow:
auto; background-color:rgba(0,0,0,0.5);
">
505 <div style="background-color:#fff; margin:5%
auto; padding:20px; border:1px solid #888; width:80%; max-width:900px; border-radius:8px; box-shadow:0 4px 8px rgba(0,0,0,0.2);
">
506 <span style="float:right; font-size:28px; font-weight:bold; cursor:pointer;
" onclick="document.getElementById(
'logModal').style.display=
'none'">×</span>
507 <h2><?php echo $langs->trans("LogDetails
"); ?></h2>
509 <h3><?php echo $langs->trans("ErrorWarning
"); ?></h3>
510 <div id="modalError
" style="background:#fff0f0; border:1px solid #ffcdd2; color:#d32f2f; padding:10px; border-radius:4px; display:none;
"></div>
512 <div style="display:flex; gap:20px; margin-top:15px;
">
513 <div style="flex:1;
">
514 <h3><?php echo $langs->trans("RequestPayload
"); ?></h3>
515 <textarea id="modalReq
" style="width:100%; height:300px; font-family:monospace; font-size:12px; border:1px solid #ccc;
" readonly></textarea>
517 <div style="flex:1;
">
518 <h3><?php echo $langs->trans("ResponsePayload
"); ?></h3>
519 <textarea id="modalRes
" style="width:100%; height:300px; font-family:monospace; font-size:12px; border:1px solid #ccc;
" readonly></textarea>
523 <div style="text-align:right; margin-top:15px;
">
524 <button class="button" onclick="document.getElementById(
'logModal').style.display=
'none'"><?php echo $langs->trans("Close
"); ?></button>
530// UTF-8 safe base64 decoding function
531function base64ToUtf8(str) {
532 // Going backwards: from bytestream, to percent-encoding, to original string.
533 return decodeURIComponent(atob(str).split('').map(function(c) {
534 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
538// Function to decode Unicode escape sequences in JSON strings
539function decodeUnicodeEscapes(str) {
540 // First, try to parse as JSON to handle escaped Unicode properly
542 // If it's a JSON string, parse and stringify to decode escapes
543 const parsed = JSON.parse(str);
544 return JSON.stringify(parsed, null, 2);
546 // If not valid JSON, try to decode Unicode escapes in the string
547 return str.replace(/\\u([0-9a-fA-F]{4})/g, function(match, p1) {
548 return String.fromCharCode(parseInt(p1, 16));
553// Sanitize HTML to prevent XSS
554function escapeHtml(text) {
555 const div = document.createElement('div');
556 div.textContent = text;
557 return div.innerHTML;
560function openLogModal(btn) {
561 // Decode Base64 safely with UTF-8 support
562 const reqBase64 = btn.getAttribute('data-req') || '';
563 const resBase64 = btn.getAttribute('data-res') || '';
564 const errBase64 = btn.getAttribute('data-err') || '';
571 // Decode Unicode escape sequences
573 req = base64ToUtf8(reqBase64);
574 req = decodeUnicodeEscapes(req);
577 res = base64ToUtf8(resBase64);
578 res = decodeUnicodeEscapes(res);
581 err = base64ToUtf8(errBase64);
582 err = decodeUnicodeEscapes(err);
585 console.error('Error decoding base64:', e);
586 // Fallback to regular atob if UTF-8 decoding fails
587 req = reqBase64 ? atob(reqBase64) : '';
588 res = resBase64 ? atob(resBase64) : '';
589 err = errBase64 ? atob(errBase64) : '';
592 // Sanitize content before setting it
593 document.getElementById('modalReq').value = req || '(<?php echo $langs->trans("NoRequestPayload
"); ?>)';
594 document.getElementById('modalRes').value = res || '(<?php echo $langs->trans("NoResponsePayload
"); ?>)';
596 const errDiv = document.getElementById('modalError');
598 errDiv.innerText = err;
599 errDiv.style.display = 'block';
601 errDiv.style.display = 'none';
604 document.getElementById('logModal').style.display = 'block';
$id
Support class for third parties, contacts, members, users or resources.
if(! $sortfield) if(! $sortorder) $object
if(!defined('NOREQUIRESOC')) if(!defined( 'NOREQUIRETRAN')) if(!defined('NOTOKENRENEWAL')) if(!defined( 'NOREQUIREMENU')) if(!defined('NOREQUIREHTML')) if(!defined( 'NOREQUIREAJAX')) llxHeader($head='', $title='', $help_url='', $target='', $disablejs=0, $disablehead=0, $arrayofjs='', $arrayofcss='', $morequerystring='', $morecssonbody='', $replacemainareaby='', $disablenofollow=0, $disablenoindex=0)
Empty header.
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
newToken()
Return the value of token currently saved into session with name 'newtoken'.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0, $nodefault=0)
Return value of a param into GET or POST supervariable.
GETPOSTINT($paramname, $method=0, $nodefault=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
multi select button
0 = Do not include form tag and submit button -1 = Do not include form tag but include submit button
print_liste_field_titre($name, $file="", $field="", $begin="", $param="", $moreattrib="", $sortfield="", $sortorder="", $prefix="", $tooltip="", $forcenowrapcolumntitle=0)
Show title line of an array.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
aiModelPrice($model)
Price (in/out, per 1M tokens, in the admin's own currency and contract) for a model,...
print $langs trans('Date')." left Ref Label right Qty right Price right TotalHT right TotalTTC right right right right right right right right right centpercent right TotalHT right n right VAT right n right TotalVAT right n No sujeto a RE IRPF right TotalLT1 right n right TotalLT2 right n right TotalTTC right n takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency right TotalTTC takeposcustomercurrency right takeposcustomercurrency n right Paid right PaymentTypeShortLIQ right SELECT p pos_change as p datep as date
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.