Nessun oggetto della modifica
Etichetta: Annullato
Nessun oggetto della modifica
Etichetta: Ripristino manuale
Riga 329: Riga 329:
     }
     }
   </script>
   </script>
<!-- =========================================================
    WIDGET "CODECARD" PER DASHBOARD – codice a colori + COPIA
    Requisiti: nessuno oltre a Internet per CDN highlight.js
    Inserisci questo blocco nella pagina Dashboard (HTML).
    ========================================================= -->
<!-- 1) Highlight.js (tema chiaro elegante) -->
<link rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/php.min.js"></script>
<!-- 2) Stili della “card” con barra azioni -->
<style>
  .codecard {
    border: 1px solid #e5e7eb; /* gray-200 */
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 4px 24px rgba(0,0,0,.04);
    margin: 18px 0;
    background: #fff;
  }
  .codecard__bar {
    display: flex; align-items: center; justify-content: space-between;
    padding: 10px 12px; background: #f8fafc; /* slate-50 */
    border-bottom: 1px solid #e5e7eb;
  }
  .codecard__title {
    font: 600 14px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    color: #0f172a; /* slate-900 */ letter-spacing: .2px;
  }
  .codecard__actions { display: flex; gap: 8px; }
  .codecard__btn {
    font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    padding: 8px 10px; border-radius: 8px; border: 1px solid #e5e7eb;
    background: #ffffff; color:#0f172a; cursor: pointer;
    transition: background .15s ease, transform .02s ease;
  }
  .codecard__btn:hover { background: #f1f5f9; }        /* slate-100 */
  .codecard__btn:active { transform: scale(0.98); }
  .codecard__btn.primary { background:#111827; color:#fff; border-color:#111827; } /* gray-900 */
  .codecard__btn.primary:hover { background:#0b1220; }
  .codecard__wrap { padding: 10px 12px 12px; background: #ffffff; }
  .codecard pre { margin:0; }
  .codecard code { font-size: 13px; }
  .codecard code.wrap { white-space: pre-wrap; word-break: break-word; }
  .codecard__hint {
    font: 12px/1.3 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    color:#475569; padding: 8px 12px 12px;
  }
</style>
<!-- 3) Markup del contenitore dove verrà montata la card -->
<div id="chapter-tool-codecard"></div>
<!-- 4) Script: crea la CodeCard e monta il codice PHP con pulsante Copia -->
<script>
(function(){
  // ==== A) Il tuo file PHP, intero, dentro un template literal ====
  const CODE_PHP = `<?php
/**
* /api/chapter_tool.php
*
* Endpoint JSON:
* {
*  "mode": "translate" | "qa" | "summarize",
*  "model": "gpt-4.1-mini" | "gpt-4o" | ...,
*  "temperature": 0.0-2.0,
*  "prompt": "string",
*  "files": ["/path/file1.pdf", "/path/file2.docx", ...],
*  "url": "https://..."
* }
*
* Output JSON:
*  - translate: { ok, mode, text, tokens_used }
*  - qa:        { ok, mode, answer, tokens_used }
*  - summarize: { ok, mode, summary:[...], keywords:[...], tokens_used }
*
* Logging: /var/log/mpai/*.json (fallback /tmp)
*/
declare(strict_types=1);
mb_internal_encoding('UTF-8');
header('Content-Type: application/json; charset=utf-8');
// ---- Config API key ----
$OPENAI_API_KEY = null;
$keysFile = __DIR__ . '/../secure_config/keys.php';
if (is_file($keysFile)) {
  require_once $keysFile; // Deve definire OPENAI_API_KEY
  if (defined('OPENAI_API_KEY')) {
    $OPENAI_API_KEY = OPENAI_API_KEY;
  }
}
if (!$OPENAI_API_KEY) {
  echo json_encode(['ok' => false, 'error' => 'Missing OPENAI_API_KEY']);
  exit;
}
// ---- Log dir ----
$LOG_DIR = '/var/log/mpai';
if (!is_dir($LOG_DIR) || !is_writable($LOG_DIR)) {
  $LOG_DIR = sys_get_temp_dir();
}
function now_id(): string {
  return date('Ymd_His') . '_' . getmypid();
}
function save_log(string $prefix, $data): void {
  global $LOG_DIR;
  $fname = $LOG_DIR . '/' . $prefix . '_' . now_id() . '.json';
  @file_put_contents($fname, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
function read_json_input(): array {
  $raw = file_get_contents('php://input');
  $data = json_decode($raw, true);
  if (!is_array($data)) $data = [];
  save_log('chapter_tool_input', ['raw' => $raw, 'parsed' => $data]);
  return $data;
}
function curl_get(string $url, int $timeout = 20): array {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_MAXREDIRS => 5,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
    CURLOPT_USERAGENT => 'Masticationpedia-ChapterTool/1.0 (+https://masticationpedia.org)',
    CURLOPT_HTTPHEADER => ['Accept: */*'],
  ]);
  $body = curl_exec($ch);
  $err  = curl_error($ch);
  $info = curl_getinfo($ch);
  curl_close($ch);
  return ['ok' => ($err === ''), 'error' => $err, 'info' => $info, 'body' => $body];
}
function normalize_whitespace(string $s): string {
  $s = preg_replace("/[ \\t]+/u", " ", $s);
  $s = preg_replace("/\\R{3,}/u", "\\n\\n", $s);
  return trim($s ?? '');
}
function html_to_text(string $html): string {
  libxml_use_internal_errors(true);
  $dom = new DOMDocument();
  $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
  @$dom->loadHTML($html);
  if (!$dom) return strip_tags($html);
  $xpath = new DOMXPath($dom);
  foreach (['script','style','noscript'] as $tag) {
    foreach ($xpath->query("//{$tag}") as $node) {
      $node->parentNode->removeChild($node);
    }
  }
  $text = $dom->textContent ?? '';
  $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  return normalize_whitespace($text);
}
function extract_text_from_pdf(string $path): array {
  $cmd = 'pdftotext -layout -q ' . escapeshellarg($path) . ' -';
  $out = @shell_exec($cmd . ' 2>&1');
  if ($out === null) return [false, "pdftotext failed or not available"];
  return [true, normalize_whitespace((string)$out)];
}
function extract_text_with_libreoffice(string $path): array {
  $tmpDir = sys_get_temp_dir() . '/ctool_' . now_id();
  @mkdir($tmpDir, 0775, true);
  $cmd = 'soffice --headless --convert-to txt --outdir ' . escapeshellarg($tmpDir) . ' ' . escapeshellarg($path);
  $out = @shell_exec($cmd . ' 2>&1');
  if ($out === null) return [false, "LibreOffice headless conversion failed or not available"];
  $txtFiles = glob($tmpDir . '/*.txt');
  if (!$txtFiles) return [false, "No TXT produced by LibreOffice"];
  $txt = @file_get_contents($txtFiles[0]);
  return [true, normalize_whitespace((string)$txt)];
}
function extract_text_from_html_file(string $path): array {
  $html = @file_get_contents($path);
  if ($html === false) return [false, "Cannot read HTML file"];
  return [true, html_to_text($html)];
}
function extract_text_from_file(string $path): array {
  if (!is_file($path) || !is_readable($path)) return [false, "File not readable: $path"];
  $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
  switch ($ext) {
    case 'pdf': return extract_text_from_pdf($path);
    case 'doc':
    case 'docx':
    case 'odt':
    case 'rtf': return extract_text_with_libreoffice($path);
    case 'html':
    case 'htm': return extract_text_from_html_file($path);
    default:
      $txt = @file_get_contents($path);
      if ($txt === false) return [false, "Cannot read file: $path"];
      return [true, normalize_whitespace((string)$txt)];
  }
}
function chunk_text(string $text, int $chunkSize = 12000): array {
  $chunks = [];
  $len = mb_strlen($text);
  for ($i=0; $i<$len; $i+=$chunkSize) $chunks[] = mb_substr($text, $i, $chunkSize);
  return $chunks;
}
function tokenize_simple(string $s): array {
  $s = mb_strtolower($s);
  $s = preg_replace('/[^\\p{L}\\p{N}\\s]/u', ' ', $s);
  $parts = preg_split('/\\s+/u', $s, -1, PREG_SPLIT_NO_EMPTY);
  return $parts ?: [];
}
function keyword_scores(array $chunks, string $query): array {
  $qTokens = array_unique(tokenize_simple($query));
  $scores = [];
  foreach ($chunks as $i => $c) {
    $toks = tokenize_simple($c);
    $count = 0;
    if ($toks) {
      $set = array_count_values($toks);
      foreach ($qTokens as $qt) if (isset($set[$qt])) $count += $set[$qt];
    }
    $scores[$i] = $count;
  }
  arsort($scores); return $scores;
}
function openai_chat(array $messages, string $model, float $temperature, int $maxTokens = 1024): array {
  global $OPENAI_API_KEY;
  $payload = [
    'model' => $model,
    'temperature' => max(0.0, min(2.0, $temperature)),
    'messages' => $messages,
    'max_tokens' => $maxTokens
  ];
  save_log('chapter_tool_payload', $payload);
  $ch = curl_init('https://api.openai.com/v1/chat/completions');
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
      'Content-Type: application/json',
      'Authorization: Bearer ' . $OPENAI_API_KEY
    ],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
    CURLOPT_CONNECTTIMEOUT => 15,
    CURLOPT_TIMEOUT => 60
  ]);
  $raw = curl_exec($ch);
  $err = curl_error($ch);
  curl_close($ch);
  $resp = $raw ? json_decode($raw, true) : null;
  save_log('chapter_tool_response', ['error' => $err, 'raw' => $raw, 'parsed' => $resp]);
  if ($err) return ['ok' => false, 'error' => "OpenAI curl error: $err"];
  if (!is_array($resp)) return ['ok' => false, 'error' => 'Invalid OpenAI response'];
  return ['ok' => true, 'data' => $resp];
}
function chat_with_continuations(array $baseMessages, string $model, float $temperature, int $maxTokens = 1024, int $maxRounds = 8): array {
  $acc = ''; $totalTokens = 0; $round = 0; $messages = $baseMessages;
  while ($round < $maxRounds) {
    $round++;
    $r = openai_chat($messages, $model, $temperature, $maxTokens);
    if (!$r['ok']) return $r;
    $resp = $r['data'];
    $choice = $resp['choices'][0] ?? null;
    if (!$choice) return ['ok' => false, 'error' => 'Malformed OpenAI response (no choices)'];
    $content = $choice['message']['content'] ?? '';
    $finish  = $choice['finish_reason'] ?? '';
    $usage  = $resp['usage'] ?? [];
    $totalTokens += (int)($usage['total_tokens'] ?? 0);
    $acc .= $content;
    if ($finish !== 'length') {
      return ['ok' => true, 'text' => $acc, 'tokens_used' => $totalTokens];
    }
    $messages[] = ['role' => 'assistant', 'content' => $content];
    $messages[] = ['role' => 'user', 'content' =>
      "Continue from where you stopped. Do not repeat. Keep the same format. No preambles."];
  }
  return ['ok' => true, 'text' => $acc, 'tokens_used' => $totalTokens, 'note' => 'maxRounds reached'];
}
// -------- Parse input --------
$in          = read_json_input();
$mode        = $in['mode'] ?? '';
$model      = $in['model'] ?? 'gpt-4.1-mini';
$temperature = isset($in['temperature']) ? (float)$in['temperature'] : 0.3;
$prompt      = trim((string)($in['prompt'] ?? ''));
$files      = is_array($in['files'] ?? null) ? $in['files'] : [];
$url        = trim((string)($in['url'] ?? ''));
if (!in_array($mode, ['translate','qa','summarize'], true)) {
  echo json_encode(['ok' => false, 'error' => 'Invalid mode']); exit;
}
if (!$prompt && $mode !== 'summarize' && !$url && empty($files)) {
  echo json_encode(['ok' => false, 'error' => 'Missing prompt or content sources']); exit;
}
// Acquire sources
$allTextParts = [];
if ($url !== '') {
  $got = curl_get($url, 30);
  if (!$got['ok']) { echo json_encode(['ok' => false, 'error' => 'URL fetch error: ' . $got['error']]); exit; }
  $ct = strtolower((string)($got['info']['content_type'] ?? ''));
  $body = (string)$got['body'];
  if (strpos($ct, 'text/html') !== false || preg_match('/\\.html?(\\?|$)/i', $url)) {
    $allTextParts[] = html_to_text($body);
  } else {
    $allTextParts[] = normalize_whitespace($body);
  }
}
foreach ($files as $f) {
  [$ok, $txtOrErr] = extract_text_from_file($f);
  if (!$ok) { echo json_encode(['ok' => false, 'error' => $txtOrErr]); exit; }
  $allTextParts[] = $txtOrErr;
}
$fullText = normalize_whitespace(implode("\\n\\n", $allTextParts));
$chunks  = $fullText ? chunk_text($fullText, 12000) : [];
try {
  if ($mode === 'translate') {
    $temperature = 0.2;
    if (!$chunks) $chunks = chunk_text($prompt, 12000);
    $translated = ''; $tokensSum = 0;
    $translateInstruction = $prompt !== '' ? $prompt
      : "Translate the following content to the target language (default: English), preserving all markup (HTML/Wikitext) exactly. Do not add preambles, notes, or extra commentary.";
    foreach ($chunks as $ck) {
      $sys = "You are a precise translation engine. Preserve all markup (HTML/Wikitext) exactly as in input. Do not add preambles.";
      $messages = [
        ['role'=>'system','content'=>$sys],
        ['role'=>'user','content'=>$translateInstruction."\\n\\n---\\n".$ck]
      ];
      $r = chat_with_continuations($messages, $model, $temperature, 1024, 8);
      if (!$r['ok']) { echo json_encode(['ok'=>false,'error'=>$r['error']??'translate error']); exit; }
      $translated .= $r['text']."\\n";
      $tokensSum += (int)($r['tokens_used'] ?? 0);
    }
    echo json_encode(['ok'=>true,'mode'=>'translate','text'=>trim($translated),'tokens_used'=>$tokensSum],
      JSON_UNESCAPED_UNICODE); exit;
  }
  if ($mode === 'qa') {
    if (!$chunks) { echo json_encode(['ok'=>false,'error'=>'No context available (no url/files text)']); exit; }
    $scores = keyword_scores($chunks, $prompt);
    $topN = 5; $pickedIdx = array_slice(array_keys($scores), 0, $topN);
    $contextPieces = [];
    foreach ($pickedIdx as $i) { $contextPieces[] = "### CHUNK #".($i+1)."\\n".$chunks[$i]; }
    $context = implode("\\n\\n", $contextPieces);
    $sys = "You are a careful scientific assistant. Answer strictly based on the provided CONTEXT. If the answer is not present, say you cannot find it in the context. Be concise and precise.";
    $userMsg = "CONTEXT:\\n".$context."\\n\\nQUESTION:\\n".$prompt."\\n\\nRULES: cite relevant chunk sections if possible; do not invent; no preambles.";
    $messages = [['role'=>'system','content'=>$sys],['role'=>'user','content'=>$userMsg]];
    $r = chat_with_continuations($messages, $model, $temperature, 1024, 8);
    if (!$r['ok']) { echo json_encode(['ok'=>false,'error'=>$r['error']??'qa error']); exit; }
    echo json_encode(['ok'=>true,'mode'=>'qa','answer'=>trim($r['text']),'tokens_used'=>(int)($r['tokens_used']??0)],
      JSON_UNESCAPED_UNICODE); exit;
  }
  if ($mode === 'summarize') {
    $temperature = max(0.0, min(2.0, (float)$temperature ?: 0.3));
    if (!$chunks) {
      if (!$prompt) { echo json_encode(['ok'=>false,'error'=>'Nothing to summarize']); exit; }
      $chunks = chunk_text($prompt, 12000);
    }
    $intermediateSummaries = []; $tokensSum = 0;
    foreach ($chunks as $ck) {
      $sys = "You are a scientific summarizer.";
      $user = "Summarize the following content into 5–7 concise bullets, no preambles. Then extract 10 keywords (single words or short bigrams), comma-separated.\\n\\nCONTENT:\\n".$ck;
      $messages = [['role'=>'system','content'=>$sys],['role'=>'user','content'=>$user]];
      $r = chat_with_continuations($messages, $model, $temperature, 900, 6);
      if (!$r['ok']) { echo json_encode(['ok'=>false,'error'=>$r['error']??'summarize chunk error']); exit; }
      $intermediateSummaries[] = trim($r['text']); $tokensSum += (int)($r['tokens_used'] ?? 0);
    }
    $mergeInput = implode("\\n\\n---\\n\\n", $intermediateSummaries);
    $sys2  = "You are a precise summarizer.";
    $user2 = "Merge the following partial summaries into ONE final output:\\n- 5–7 concise bullets.\\n- Then a line with exactly 10 keywords (comma-separated).\\nNo preambles.\\n\\nPARTIALS:\\n".$mergeInput;
    $r2 = chat_with_continuations([['role'=>'system','content'=>$sys2],['role'=>'user','content'=>$user2]], $model, $temperature, 700, 4);
    if (!$r2['ok']) { echo json_encode(['ok'=>false,'error'=>$r2['error']??'summarize merge error']); exit; }
    $tokensSum += (int)($r2['tokens_used'] ?? 0);
    $final = trim($r2['text']); $lines = preg_split('/\\R/u', $final);
    $bullets = []; $keywords = [];
    foreach ($lines as $ln) {
      $t = trim($ln); if ($t==='') continue;
      if (preg_match('/^[-*•]\\s*(.+)$/u', $t, $m)) { $bullets[] = trim($m[1]); }
      else if (!count($keywords) && preg_match('/[,,]/u', $t)) {
        $parts = preg_split('/\\s*[,,]\\s*/u', $t);
        foreach ($parts as $p) { $p = trim($p); if ($p!=='') $keywords.push($p); }
      }
    }
    if (count($bullets)>7) $bullets = array_slice($bullets,0,7);
    if (count($keywords)>10) $keywords = array_slice($keywords,0,10);
    echo json_encode(['ok'=>true,'mode'=>'summarize','summary'=>$bullets,'keywords'=>$keywords,'tokens_used'=>$tokensSum],
      JSON_UNESCAPED_UNICODE); exit;
  }
  echo json_encode(['ok'=>false,'error'=>'Unhandled mode']);
} catch (Throwable $e) {
  echo json_encode(['ok'=>false,'error'=>'Unhandled exception: '.$e->getMessage()]);
  exit;
}
?>`;
  // ==== B) Funzione che crea la card ====
  function createCodeCard({ title, filename, language, code }) {
    const container = document.createElement('div');
    container.className = 'codecard';
    container.innerHTML = `
      <div class="codecard__bar">
        <div class="codecard__title">${title}</div>
        <div class="codecard__actions">
          <button class="codecard__btn" data-action="wrap">Wrap</button>
          <button class="codecard__btn" data-action="download">Scarica</button>
          <button class="codecard__btn primary" data-action="copy">Copia</button>
        </div>
      </div>
      <div class="codecard__wrap">
        <pre><code class="${language}" id="codecard-code"></code></pre>
      </div>
      <div class="codecard__hint">Suggerimento: clicca <b>Copia</b> per incollare velocemente il file nella tua cartella <code>/api/</code>.</div>
    `;
    const codeEl = container.querySelector('#codecard-code');
    codeEl.textContent = code;
    // Evidenziazione sintattica
    if (window.hljs) { hljs.highlightElement(codeEl); }
    // Azioni
    container.querySelector('[data-action="copy"]').addEventListener('click', async () => {
      try {
        await navigator.clipboard.writeText(code);
        const btn = container.querySelector('[data-action="copy"]');
        const old = btn.textContent;
        btn.textContent = 'Copiato!';
        setTimeout(()=> btn.textContent = old, 1200);
      } catch(e) { alert('Copia non riuscita: ' + e.message); }
    });
    container.querySelector('[data-action="download"]').addEventListener('click', () => {
      const blob = new Blob([code], { type: 'text/plain;charset=utf-8' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = filename || 'chapter_tool.php';
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
      setTimeout(()=> URL.revokeObjectURL(a.href), 1500);
    });
    let wrapped = false;
    container.querySelector('[data-action="wrap"]').addEventListener('click', () => {
      wrapped = !wrapped;
      if (wrapped) codeEl.classList.add('wrap'); else codeEl.classList.remove('wrap');
    });
    return container;
  }
  // ==== C) Monta la card nella Dashboard ====
  const mount = document.getElementById('chapter-tool-codecard');
  if (mount) {
    const card = createCodeCard({
      title: 'chapter_tool.php (endpoint API)',
      filename: 'chapter_tool.php',
      language: 'language-php',
      code: CODE_PHP
    });
    mount.appendChild(card);
  }
})();
</script>
</html>
</html>

Versione delle 22:39, 17 ott 2025

🔧 Dashboard Operativa – Masticationpedia

Centro di comando per progetti, API, file e backup

🧾 Apri Log Dashboard