0, 'path' => '/', 'domain' => '', 'secure' => $secureCookies, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); // ------------------------------ // Vendor autoload (Markdown + Sanitizer) // ------------------------------ require_once __DIR__ . '/vendor/autoload.php'; // Lightweight JSON responder function json_out(array $data, int $code = 200): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } // CSRF token helpers function csrf_token(): string { if (empty($_SESSION['__csrf'])) { $_SESSION['__csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['__csrf']; } function require_csrf(): void { $t = $_POST['csrf'] ?? ''; if (!hash_equals($_SESSION['__csrf'] ?? '', (string)$t)) { json_out(['ok' => false, 'error' => 'CSRF token invalid'], 419); } } // ------------------------------ // 1) Backend: LLM function stub // ------------------------------ if (!function_exists('llm')) { /** * LLM call * @param string $prompt * @param string|null $system_message * @param array $previousConversation */ require_once __DIR__."/../LlamaCli.func.php"; function llm(string $prompt, ?string $system_message = "You are a helpful assistant", array $previousConversation = []): string { $opts = []; if (!empty($previousConversation)) { // Optionally cap context length to the last N turns $N = 20; if (count($previousConversation) > $N) { $previousConversation = array_slice($previousConversation, -$N); } $opts['previousConversation'] = $previousConversation; } return LlamaCli_raw($prompt, $system_message ?? "", $opts); } } // ------------------------------ // Markdown -> HTML + sanitize // ------------------------------ /** @return array{html:string, used_sanitizer:bool} */ function md_to_safe_html(string $markdown): array { $html = $markdown; $used = false; // Convert Markdown to HTML (GitHub-flavored if available) if (class_exists(League\CommonMark\GithubFlavoredMarkdownConverter::class)) { $converter = new League\CommonMark\GithubFlavoredMarkdownConverter([ 'html_input' => 'strip', // ignore raw HTML from model 'allow_unsafe_links' => false, 'max_nesting_level' => 20, ]); $html = (string)$converter->convert($markdown); } else { // Fallback: escape ->
(so we never inject unsafe HTML)
//$html = ''.htmlspecialchars($markdown, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').'
';
$html = 'ERROR: GitHubFlavoredMarkdownConverter not available
';
}
// Sanitize HTML
if (class_exists(\HTMLPurifier::class)) {
$config = \HTMLPurifier_Config::createDefault();
// allow common formatting + code + tables + kbd
$config->set('Cache.DefinitionImpl', null);
$config->set('HTML.Allowed', implode(',', [
'p','br','hr','blockquote','strong','em','del','ins','u','s','sup','sub','kbd',
'pre','code','span','ol','ul','li',
'h1','h2','h3','h4','h5','h6',
'table','thead','tbody','tfoot','tr','th','td',
'a[href|title|target]','img[src|alt|title|width|height]',
]));
$config->set('URI.SafeIframeRegexp', '%^https?://%'); // if you ever allow iframes later
$config->set('Attr.AllowedFrameTargets', ['_blank']);
$config->set('AutoFormat.AutoParagraph', false);
$purifier = new \HTMLPurifier($config);
$html = $purifier->purify($html);
$used = true;
}
return ['html' => $html, 'used_sanitizer' => $used];
}
// --------------------------------
// 2) Helpers for chat persistence
// --------------------------------
const SESSION_KEY = 'mini_chatgpt_history';
if (!isset($_SESSION[SESSION_KEY])) {
$_SESSION[SESSION_KEY] = [];
}
/**
* @return array
*/
function chat_history(): array {
return $_SESSION[SESSION_KEY];
}
/**
* @param array $extra
*/
function chat_append(string $role, string $content, array $extra = []): void {
$_SESSION[SESSION_KEY][] = array_merge([
'role' => $role,
'content' => $content,
'ts' => time(),
], $extra);
}
function chat_reset(): void {
$_SESSION[SESSION_KEY] = [];
}
// ------------------------------
// 3) AJAX endpoints (POST only)
// ------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_csrf();
$action = $_POST['action'] ?? '';
// Reset
if ($action === 'reset') {
chat_reset();
json_out(['ok' => true]);
}
// Theme change
if ($action === 'set_theme') {
$theme = $_POST['theme'] ?? '';
if (!in_array($theme, ['light','dark'], true)) {
json_out(['ok' => false, 'error' => 'Invalid theme'], 422);
}
$_SESSION['__theme'] = $theme;
json_out(['ok' => true, 'theme' => $theme]);
}
// Chat
if ($action === 'chat') {
$message = trim((string)($_POST['message'] ?? ''));
$system = isset($_POST['system']) ? trim((string)$_POST['system']) : null;
if ($message === '') {
json_out(['ok' => false, 'error' => 'Empty message'], 422);
}
if (mb_strlen($message) > 4000) {
json_out(['ok' => false, 'error' => 'Message too long'], 413);
}
if ($system !== null && mb_strlen($system) > 4000) {
json_out(['ok' => false, 'error' => 'System too long'], 413);
}
// Persist optional system prompt
if ($system !== null && $system !== '') {
$_SESSION['__system_prompt'] = $system;
}
$effectiveSystem = $_SESSION['__system_prompt'] ?? ($system ?: null);
// Build previousConversation from existing session history
$historyRaw = chat_history();
$previousConversation = [];
foreach ($historyRaw as $turn) {
if (!isset($turn['role'], $turn['content'])) continue;
if ($turn['role'] !== 'user' && $turn['role'] !== 'assistant') continue;
$previousConversation[] = [
'role' => $turn['role'],
'content' => (string)$turn['content'],
];
}
// Record current user message
chat_append('user', $message);
// Ask the model
$reply_raw = llm($message, $effectiveSystem, $previousConversation);
// Convert markdown -> sanitized HTML
$md = md_to_safe_html($reply_raw);
$reply_html = $md['html'];
// Save assistant reply (store both raw + html)
chat_append('assistant', $reply_raw, ['content_html' => $reply_html]);
json_out([
'ok' => true,
'reply' => $reply_raw,
'reply_html'=> $reply_html,
'history' => chat_history(),
]);
}
json_out(['ok' => false, 'error' => 'Unknown action'], 400);
}
// ------------------------------
// 4) HTML (Bootstrap 5)
// ------------------------------
$history = chat_history();
$systemPrefill = htmlspecialchars($_SESSION['__system_prompt'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$theme = htmlspecialchars($_SESSION['__theme'] ?? 'dark', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$csrf = csrf_token();