= 400) { $msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown API error'; throw new RuntimeException("OpenAI API error ({$code}): {$msg}"); } return is_array($data) ? $data : []; } /** Create or reuse a per-visitor thread */ function ensure_thread_id() { if (!empty($_SESSION['thread_id'])) return $_SESSION['thread_id']; $created = openai_request('POST', '/threads', ['metadata' => ['site' => $_SERVER['HTTP_HOST'] ?? 'unknown']]); $tid = $created['id'] ?? null; if (!$tid) throw new RuntimeException('Failed to create thread.'); $_SESSION['thread_id'] = $tid; return $tid; } /** Add a user message */ function add_user_message($thread_id, $text) { openai_request('POST', "/threads/{$thread_id}/messages", [ 'role' => 'user', 'content' => $text, ]); } /** Start a run */ function start_run($thread_id, $assistant_id) { $run = openai_request('POST', "/threads/{$thread_id}/runs", [ 'assistant_id' => $assistant_id, ]); $run_id = $run['id'] ?? null; if (!$run_id) throw new RuntimeException('Failed to start run.'); return $run_id; } /** Wait for completion (or fail/timeout) */ function wait_for_run($thread_id, $run_id, $max_tries, $delay_us) { $terminal = ['completed', 'failed', 'requires_action', 'cancelled', 'expired']; for ($i = 0; $i < $max_tries; $i++) { usleep($delay_us); $run = openai_request('GET', "/threads/{$thread_id}/runs/{$run_id}"); $status = $run['status'] ?? ''; if (in_array($status, $terminal, true)) return $run; } return ['status' => 'timeout']; } /** Cache of file_id => filename (per request) */ $_FILE_NAME_CACHE = []; /** Resolve file name from file_id (API returns "filename" or sometimes "display_name") */ function get_file_name_by_id($file_id) { global $_FILE_NAME_CACHE; if (isset($_FILE_NAME_CACHE[$file_id])) return $_FILE_NAME_CACHE[$file_id]; $file = openai_request('GET', "/files/{$file_id}"); $name = $file['filename'] ?? ($file['display_name'] ?? ($file['name'] ?? $file_id)); $_FILE_NAME_CACHE[$file_id] = $name; return $name; } /** * Extract message text + citations (filename + page if available). * Returns an array of entries: ['role' => 'user|assistant', 'text' => '...', 'refs' => [['filename'=>'','page'=>'','file_id'=>'']]] */ function normalize_messages($messages) { $out = []; if (empty($messages['data']) || !is_array($messages['data'])) return $out; // The API returns newest first by default if not specifying; we request 'asc' in fetch. foreach ($messages['data'] as $m) { $role = $m['role'] ?? ''; if (!in_array($role, ['user', 'assistant', 'system'], true)) continue; if (empty($m['content']) || !is_array($m['content'])) continue; $all_text = []; $refs = []; foreach ($m['content'] as $part) { if (($part['type'] ?? '') === 'text' && !empty($part['text']['value'])) { $all_text[] = $part['text']['value']; // Parse annotations for citations (file_citation) $anns = $part['text']['annotations'] ?? []; if (is_array($anns)) { foreach ($anns as $ann) { if (($ann['type'] ?? '') === 'file_citation' && !empty($ann['file_citation']['file_id'])) { $fid = $ann['file_citation']['file_id']; $page = null; // Page can appear under different shapes depending on backend. Try common keys: if (isset($ann['file_citation']['page'])) { $page = $ann['file_citation']['page']; } elseif (isset($ann['file_citation']['page_range']) && is_array($ann['file_citation']['page_range'])) { // Example: ['start' => 5, 'end' => 6] $start = $ann['file_citation']['page_range']['start'] ?? null; $end = $ann['file_citation']['page_range']['end'] ?? null; if ($start && $end && $start !== $end) $page = "{$start}-{$end}"; elseif ($start) $page = (string)$start; } // Fetch filename try { $filename = get_file_name_by_id($fid); } catch (Throwable $e) { $filename = $fid; } $refs[] = [ 'file_id' => $fid, 'filename' => $filename, 'page' => $page ?? 'n/a', ]; } } } } } if (!empty($all_text)) { $out[] = [ 'role' => $role, 'text' => implode("\n", $all_text), 'refs' => $refs, ]; } } return $out; } /** Fetch conversation (ascending) */ function fetch_history($thread_id) { $messages = openai_request('GET', "/threads/{$thread_id}/messages", null, ['order' => 'asc', 'limit' => 50]); return normalize_messages($messages); } /* ------------------- HANDLE POST ------------------- */ $error = null; $history = []; try { if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!empty($_POST['reset_thread'])) { $_SESSION['thread_id'] = null; } elseif (isset($_POST['user_input'])) { $user_text = trim((string)$_POST['user_input']); if ($user_text !== '') { $thread_id = ensure_thread_id(); add_user_message($thread_id, $user_text); $run_id = start_run($thread_id, $ASSISTANT_ID); $run = wait_for_run($thread_id, $run_id, $POLL_MAX_TRIES, $POLL_DELAY_US); if (($run['status'] ?? '') === 'failed') { $error = 'Assistant run failed.'; } elseif (($run['status'] ?? '') === 'requires_action') { // If you later support tool calls, handle them here then submit outputs. } elseif (($run['status'] ?? '') === 'timeout') { $error = 'Assistant timed out. Please try again.'; } } } } if (!empty($_SESSION['thread_id'])) { $history = fetch_history($_SESSION['thread_id']); } } catch (Throwable $e) { $error = $e->getMessage(); } ?>

Site Assistant

Type a question below. Press Enter to send, Shift+Enter for a new line.

Error:
Thread:
Question, assistant => Answer, system => (optional) $role = $msg['role'] ?? 'assistant'; if ($role === 'user') $label = 'Question'; elseif ($role === 'assistant') $label = 'Answer'; else $label = ucfirst($role); // e.g., System $text = str_replace("\r\n", "\n", $msg['text'] ?? ''); $refs = $msg['refs'] ?? []; ?>
References:
No messages yet.
Conversation persists until you click “Reset Conversation”.