|
|
| (Una versione intermedia di uno stesso utente non è mostrata) |
| Riga 168: |
Riga 168: |
| <pre id="api-result" style="background:#f0f0f0; padding:1rem; border:1px solid #ccc; margin-top:1rem; white-space:pre-wrap;"></pre> | | <pre id="api-result" style="background:#f0f0f0; padding:1rem; border:1px solid #ccc; margin-top:1rem; white-space:pre-wrap;"></pre> |
| </div> | | </div> |
|
| |
| <!-- ========== JS (copia codice, parser blocchi, invio) ========== -->
| |
| <script>
| |
| function toggleDashboardBox(id){
| |
| const el = document.getElementById(id);
| |
| if (!el) return;
| |
| el.style.display = (el.style.display === 'none' || !el.style.display) ? 'block' : 'none';
| |
| if (el.style.display === 'block') el.scrollIntoView({behavior:'smooth'});
| |
| }
| |
| function showMpAI(){ document.getElementById('mpAI').style.display='block'; }
| |
|
| |
| function mpCopy(btn){
| |
| try{
| |
| const box = btn.closest('.mp-code');
| |
| const pre = box.querySelector('pre');
| |
| const txt = pre ? pre.innerText : '';
| |
| if (!txt) throw new Error('Codice non trovato');
| |
| navigator.clipboard.writeText(txt).then(()=>{
| |
| const old = btn.textContent;
| |
| btn.textContent = '✅ Copiato';
| |
| setTimeout(()=>btn.textContent = old, 1200);
| |
| });
| |
| }catch(e){ alert('Copia non riuscita: '+e.message); }
| |
| }
| |
|
| |
| // Trasforma testo con blocchi ```lang\n...\n``` in nodi HTML con cornice + copia
| |
| function renderTextWithCodeBlocks(text){
| |
| const frag = document.createDocumentFragment();
| |
| const re = /```(\w+)?\n([\s\S]*?)```/g;
| |
| let lastIndex = 0, m;
| |
| while ((m = re.exec(text)) !== null){
| |
| const before = text.slice(lastIndex, m.index).trim();
| |
| if (before){
| |
| const p = document.createElement('div');
| |
| p.textContent = before;
| |
| frag.appendChild(p);
| |
| }
| |
| const lang = (m[1] || 'code').toLowerCase();
| |
| const code = m[2].replace(/\n+$/,'');
| |
| const box = document.createElement('div'); box.className = 'mp-code';
| |
| const hdr = document.createElement('div'); hdr.className = 'mp-code__hdr';
| |
| hdr.innerHTML = `<span class="mp-code__lang">${lang}</span>`;
| |
| const btn = document.createElement('button'); btn.className='mp-copy'; btn.textContent='📋 Copia'; btn.onclick=()=>mpCopy(btn);
| |
| hdr.appendChild(btn);
| |
| const pre = document.createElement('pre'); pre.textContent = code;
| |
| box.appendChild(hdr); box.appendChild(pre);
| |
| frag.appendChild(box);
| |
| lastIndex = re.lastIndex;
| |
| }
| |
| const tail = text.slice(lastIndex).trim();
| |
| if (tail){
| |
| const p = document.createElement('div'); p.textContent = tail;
| |
| frag.appendChild(p);
| |
| }
| |
| return frag;
| |
| }
| |
|
| |
| (function(){
| |
| const $chat = document.getElementById('mpai-chat');
| |
| const $input = document.getElementById('mpai-input');
| |
| const $send = document.getElementById('mpai-send');
| |
|
| |
| function addMsg(role, content){
| |
| const wrap = document.createElement('div');
| |
| wrap.className = 'mpai-msg ' + (role === 'user' ? 'user' : (role === 'assistant' ? 'assistant' : 'error'));
| |
| const head = document.createElement('div'); head.className = 'role'; head.textContent = role;
| |
| const body = document.createElement('div'); body.className = 'body';
| |
|
| |
| if (role === 'assistant'){
| |
| body.appendChild(renderTextWithCodeBlocks(String(content || '')));
| |
| } else {
| |
| body.textContent = String(content || '');
| |
| }
| |
|
| |
| wrap.appendChild(head); wrap.appendChild(body);
| |
| $chat.appendChild(wrap);
| |
| $chat.scrollTop = $chat.scrollHeight;
| |
| }
| |
|
| |
| async function sendMessage(){
| |
| const prompt = ($input.value || '').trim();
| |
| if (!prompt) return;
| |
|
| |
| addMsg('user', prompt);
| |
| $input.value = '';
| |
|
| |
| const model = document.getElementById('mpai-model')?.value || 'gpt-4o-2024-05-13';
| |
| const temperature = parseFloat(document.getElementById('mpai-temp')?.value || '0.7');
| |
| const drop = document.getElementById('mpai-drop-urls')?.checked === true;
| |
|
| |
| const files = [];
| |
| const texts = [];
| |
|
| |
| const payload = { model, prompt, temperature, drop_urls: drop, files, texts };
| |
|
| |
| try {
| |
| const res = await fetch('/dashboard/api/openai_project_gpt.php', {
| |
| method: 'POST',
| |
| headers: { 'Content-Type': 'application/json' },
| |
| credentials: 'include',
| |
| body: JSON.stringify(payload)
| |
| });
| |
| const ct = res.headers.get('content-type') || '';
| |
| let out;
| |
| if (ct.includes('application/json')){
| |
| const j = await res.json();
| |
| out = j?.text ?? j?.message ?? (typeof j === 'string' ? j : JSON.stringify(j, null, 2));
| |
| } else {
| |
| out = await res.text();
| |
| }
| |
| addMsg('assistant', out || '(vuoto)');
| |
| } catch (e) {
| |
| addMsg('error', String(e));
| |
| }
| |
| }
| |
|
| |
| $send?.addEventListener('click', sendMessage);
| |
| $input?.addEventListener('keydown', (e) => {
| |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) sendMessage();
| |
| });
| |
|
| |
| document.getElementById('mpai-clear')?.addEventListener('click', ()=>{
| |
| $chat.innerHTML = '<div class="mpai-msg mpai-hint">Conversazione pulita.</div>';
| |
| });
| |
| })();
| |
|
| |
| // -------- pannello test “Connessione API” (force translate) -------
| |
| document.getElementById('test_run')?.addEventListener('click', async () => {
| |
| const model = document.getElementById('model-select').value;
| |
| const prompt = document.getElementById('test_prompt').value;
| |
| const drop = document.getElementById('test_drop_urls').checked;
| |
|
| |
| const payload = {
| |
| model,
| |
| prompt,
| |
| temperature: 0.7,
| |
| drop_urls: drop,
| |
| files: [],
| |
| texts: [],
| |
| mode: 'translate' // forza ramo traduzione per il test
| |
| };
| |
|
| |
| try {
| |
| const res = await fetch('/dashboard/api/openai_project_gpt.php', {
| |
| method: 'POST',
| |
| headers: { 'Content-Type': 'application/json' },
| |
| credentials: 'include',
| |
| body: JSON.stringify(payload)
| |
| });
| |
| const j = await res.json();
| |
| document.getElementById('api-result').textContent = JSON.stringify(j, null, 2);
| |
| } catch (e) {
| |
| document.getElementById('api-result').textContent = String(e);
| |
| }
| |
| });
| |
|
| |
| // Utility: svuota log lato server (se l’hai implementato)
| |
| function clearServerLog(){
| |
| fetch('/dashboard/api/log_clear.php', {method:'POST', credentials:'include'}).then(()=>alert('Log svuotato'));
| |
| }
| |
| </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> | | <script> |
| (function(){
| | // JS minimo temporaneo per sbloccare la pagina |
| // ==== A) Il tuo file PHP, intero, dentro un template literal ====
| | // (serve solo a NON avere errori di sintassi) |
| 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> | | </script> |
|
| |
|
| | | |
| | |
| | |
| </html> | | </html> |