<?php
include('./setup.php');
header('Content-Type: application/json; charset=utf-8');
$payloadEncoded = $_POST['payload'] ?? '';
$payload = $payloadEncoded ? json_decode(base64_decode($payloadEncoded), true) : [];
$action = $_POST['action'] ?? '';
$chatDir = __DIR__ . '/chats/';
$indexFile = __DIR__ . '/chats.json';
if (!file_exists($chatDir)) {
mkdir($chatDir, 0777, true);
}
$response = array("status" => "error", "message" => "Unknown action");
switch ($action) {
case "chat":
// Note: Since FormData sends data via multipart/form-data, use $_POST instead of $payload for text fields
$prompt = $_POST['prompt'] ?? '';
$model = $_POST['model'] ?? 'qwen3';
$vmodel = $_POST['vmodel'] ?? 'qwen2.5-vision';
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $_POST['chat_id'] ?? ('chat-' . time()));
// 1. Handle File Uploads
$attachmentPaths = array();
$isImageUpload = false;
$fileContextContent = "";
$base64Images = array(); // Array to store multiple images
if (isset($_FILES['attachment']) && !empty($_FILES['attachment']['name'])) {
$uploadDir = __DIR__ . "/chats/{$chatId}/uploads/";
if (!is_dir($uploadDir)) mkdir($uploadDir, 0777, true);
if (!isset($fileContextContent)) $fileContextContent = "";
foreach ($_FILES['attachment']['error'] as $key => $error) {
if ($error === UPLOAD_ERR_OK) {
$rawFileName = $_FILES['attachment']['name'][$key];
$fileName = preg_replace('/[^a-zA-Z0-9\.\-_]/', '_', $rawFileName);
$targetFile = $uploadDir . time() . '_' . $key . '_' . $fileName;
if (move_uploaded_file($_FILES['attachment']['tmp_name'][$key], $targetFile)) {
$attachmentPaths[] = './chats/' . $chatId . '/uploads/' . basename($targetFile);
$mimeType = mime_content_type($targetFile);
if (strpos($mimeType, 'image/') === 0) {
$isImageUpload = true;
}
}
}
}
}
$backendUrl = 'http://'.$BACKEND_IP.':'.$BACKEND_AI_PORT.'/api/chat';
$modelMap = [
'twil-8b' => 'twil-8b:latest',
'liquid-1.7b' => 'lfm3-1.7b',
'qwen3' => 'qwen2.5:7b-instruct-q4_K_M',
'deepseek-uncensored' => 'deepseek-uncensored',
'gemma4-uncensored' => 'gemma4-uncensored',
'qwen2.5-vision' => 'qwen2.5-vision',
'qwen2.5vl' => 'qwen2.5vl:7b',
'llama3.2-vision' => 'llama3.2-vision:latest'
];
// Automatically route to a vision model if an image is attached
$actualModel = $modelMap[$model] ?? 'gemma4-uncensored';
if ($isImageUpload) {
//$actualModel = 'llama3.2-vision:latest'; // Ensure your Ollama host has this pulled
$actualModel = $modelMap[$vmodel]; // Ensure your Ollama host has this pulled
}
$job = [
"job_id" => "job-" . time() . "-" . mt_rand(100, 999),
"chat_id" => $chatId,
"type" => "ai_chat",
"payload" => [
"prompt" => $prompt,
"model" => $actualModel,
"attachments" => $attachmentPaths,
"is_upload" => $isImageUpload
],
"status" => "pending",
"time" => time()
];
// Append to queue.json and fire worker
enqueueJob($job);
updateChatStatus($chatId, 'thinking'); // ai version generating
// Immediately respond to frontend that the job is safely queued
$response = array("status" => "success", "chat_id" => $chatId);
break;
case "generate_image":
$prompt = $_POST['prompt'] ?? ($payload['prompt'] ?? '');
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $_POST['chat_id'] ?? ($payload['chat_id'] ?? ('chat-' . time())));
$width = intval($_POST['width'] ?? ($payload['width'] ?? 512));
$height = intval($_POST['height'] ?? ($payload['height'] ?? 512));
$steps = intval($_POST['steps'] ?? ($payload['steps'] ?? 20));
$attachmentPaths = array();
$isImageUpload = false;
if (isset($_FILES['attachment']) && !empty($_FILES['attachment']['name'])) {
$uploadDir = __DIR__ . "/chats/{$chatId}/uploads/";
if (!is_dir($uploadDir)) mkdir($uploadDir, 0777, true);
foreach ($_FILES['attachment']['error'] as $key => $error) {
if ($error === UPLOAD_ERR_OK) {
$rawFileName = $_FILES['attachment']['name'][$key];
$fileName = preg_replace('/[^a-zA-Z0-9\.\-_]/', '_', $rawFileName);
$targetFile = $uploadDir . time() . '_' . $key . '_' . $fileName;
if (move_uploaded_file($_FILES['attachment']['tmp_name'][$key], $targetFile)) {
$attachmentPaths[] = './chats/' . $chatId . '/uploads/' . basename($targetFile);
$mimeType = mime_content_type($targetFile);
if (strpos($mimeType, 'image/') === 0) {
$isImageUpload = true;
}
}
}
}
}
// Build lightweight job payload with disk paths instead of base64 strings
$job = [
"job_id" => "job-" . time() . "-" . mt_rand(100, 999),
"chat_id" => $chatId,
"type" => "generate_image",
"payload" => [
"prompt" => $prompt,
"steps" => $steps,
"width" => $width,
"height" => $height,
"attachments" => $attachmentPaths,
"is_upload" => $isImageUpload
],
"status" => "pending",
"time" => time()
];
enqueueJob($job);
updateChatStatus($chatId, 'generating');
$response = array("status" => "success", "chat_id" => $chatId);
break;
case "cancel_chat_state":
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
updateChatStatus($chatId, 'idle');
$response = array("status" => "success");
break;
case "load_history_list":
$chats = [];
if (file_exists($indexFile)) {
$chats = json_decode(file_get_contents($indexFile), true) ?: [];
}
usort($chats, function($a, $b) { return $b['time'] - $a['time']; });
$response = array("status" => "success", "chats" => $chats);
break;
case "load_chat":
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
$filePath = $chatDir . $chatId . '.json';
$messages = [];
if (file_exists($filePath)) {
$rawContent = file_get_contents($filePath);
$messages = json_decode($rawContent, true) ?: [];
// Self-healing check: Ensure generated_prev JPEGs exist for historical PNGs
$updated = false;
foreach ($messages as &$msg) {
if (!empty($msg['is_image']) && !empty($msg['content'])) {
$pngPath = $msg['content']; // e.g. ./chats/chat-ID/generated/img-XXX.png
// Convert relative path to absolute or relative check
//$fixed=str_replace("\\","/",$pngPath); // windows fix if needed
$localPng = __DIR__ . '/' . ltrim($pngPath, './');
$localJpg = str_replace(['/generated/', '.png'], ['/generated_prev/', '.jpg'], $localPng);
if (file_exists($localPng) && !file_exists($localJpg)) {
$prevDir = dirname($localJpg);
if (!is_dir($prevDir)) mkdir($prevDir, 0777, true);
$srcImage = @imagecreatefrompng($localPng);
if ($srcImage) {
imagejpeg($srcImage, $localJpg, 75);
imagedestroy($srcImage);
$updated = true;
}
}
}
}
unset($msg);
// Save back if any historical thumbnails were auto-generated
if ($updated) {
file_put_contents($filePath, json_encode($messages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
}
$title = $chatId;
$status = 'idle';
if (file_exists($indexFile)) {
$index = json_decode(file_get_contents($indexFile), true) ?: [];
foreach ($index as $item) {
if ($item['id'] === $chatId) {
$title = $item['title'];
$status = $item['status'] ?? 'idle';
break;
}
}
}
while (ob_get_level()) { ob_end_clean(); }
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array("status" => "success", "messages" => $messages, "title" => $title, "chat_status" => $status));
exit;
case "rename_chat":
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
$newTitle = $payload['title'] ?? 'Untitled Chat';
$index = [];
if (file_exists($indexFile)) {
$index = json_decode(file_get_contents($indexFile), true) ?: [];
}
foreach ($index as &$item) {
if ($item['id'] === $chatId) {
$item['title'] = $newTitle;
break;
}
}
file_put_contents($indexFile, json_encode($index, JSON_PRETTY_PRINT));
$response = array("status" => "success");
break;
case "get_current_sd_model":
$ch = curl_init('http://'.$BACKEND_IP.':'.$BACKEND_IMG_PORT.'/sdapi/v1/options');
curl_setopt($ch, CURLOPT_TIMEOUT, $curl_timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $curl_connout);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$apiResult = curl_exec($ch);
curl_close($ch);
$currentModel = '';
if ($apiResult) {
$options = json_decode($apiResult, true);
$currentModel = $options['sd_model_checkpoint'] ?? '';
}
$response = array("status" => "success", "current_model" => $currentModel);
break;
case "get_sd_models":
$ch = curl_init('http://'.$BACKEND_IP.':'.$BACKEND_IMG_PORT.'/sdapi/v1/sd-models');
curl_setopt($ch, CURLOPT_TIMEOUT, $curl_timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $curl_connout);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$apiResult = curl_exec($ch);
curl_close($ch);
if ($apiResult) {
$models = json_decode($apiResult, true);
$response = array("status" => "success", "models" => $models);
} else {
$response = array("status" => "error", "message" => "Could not fetch SD models.");
}
break;
case "set_sd_model":
$modelTitle = $payload['title'] ?? '';
$ch = curl_init('http://'.$BACKEND_IP.':'.$BACKEND_IMG_PORT.'/sdapi/v1/options');
curl_setopt($ch, CURLOPT_TIMEOUT, $curl_timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $curl_connout);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["sd_model_checkpoint" => $modelTitle]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_exec($ch);
curl_close($ch);
$response = array("status" => "success");
break;
case "delete_chat":
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
$filePath = $chatDir . $chatId . '.json';
if (file_exists($filePath)) { unlink($filePath); }
$index = [];
if (file_exists($indexFile)) {
$index = json_decode(file_get_contents($indexFile), true) ?: [];
}
$index = array_filter($index, function($item) use ($chatId) {
return $item['id'] !== $chatId;
});
file_put_contents($indexFile, json_encode(array_values($index), JSON_PRETTY_PRINT));
$response = array("status" => "success");
break;
case "delete_message":
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
$msgIndex = intval($payload['message_index'] ?? -1);
$filePath = $chatDir . $chatId . '.json';
if (file_exists($filePath) && $msgIndex >= 0) {
$messages = json_decode(file_get_contents($filePath), true) ?: [];
if (isset($messages[$msgIndex])) {
$targetMsg = $messages[$msgIndex];
// If it's an image message, delete both the full PNG and preview JPG from disk
if (!empty($targetMsg['is_image']) && !empty($targetMsg['content'])) {
$pngPath = __DIR__ . '/' . ltrim($targetMsg['content'], './');
$jpgPath = str_replace(['/generated/', '.png'], ['/generated_prev/', '.jpg'], $pngPath);
if (file_exists($pngPath)) @unlink($pngPath);
if (file_exists($jpgPath)) @unlink($jpgPath);
}
// Remove message from array
array_splice($messages, $msgIndex, 1);
// Save updated ledger back to disk
file_put_contents($filePath, json_encode($messages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), LOCK_EX);
$response = array("status" => "success");
} else {
$response = array("status" => "error", "message" => "Message index not found.");
}
} else {
$response = array("status" => "error", "message" => "Chat file not found.");
}
break;
case "show_more":
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
$msgIndex = intval($payload['message_index'] ?? -1);
$filePath = $chatDir . $chatId . '.json';
if (file_exists($filePath) && $msgIndex >= 0) {
$messages = json_decode(file_get_contents($filePath), true) ?: [];
if (isset($messages[$msgIndex])) {
$targetMsg = $messages[$msgIndex];
$response = array("status" => "success", "message" => $targetMsg['content'], "attachment" => $targetMsg['attachment'], "message_index" => $msgIndex);
} else {
$response = array("status" => "error", "message" => "Message index not found.");
}
} else {
$response = array("status" => "error", "message" => "Chat file not found.");
}
break;
case "upscale_image":
$chatId = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
$msgIndex = intval($payload['message_index'] ?? -1);
$filePath = $chatDir . $chatId . '.json';
// Start precise sub-second timer
$tstart = microtime(true);
if (file_exists($filePath) && $msgIndex >= 0) {
$messages = json_decode(file_get_contents($filePath), true) ?: [];
if (isset($messages[$msgIndex]) && !empty($messages[$msgIndex]['is_image'])) {
$targetMsg = $messages[$msgIndex];
$pngRelativePath = $targetMsg['content'];
$localPng = __DIR__ . '/' . ltrim($pngRelativePath, './');
if (file_exists($localPng)) {
$job = [
"job_id" => "job-" . time() . "-" . mt_rand(100, 999),
"chat_id" => $chatId,
"type" => "upscale_image",
"payload" => [
"message_index" => $msgIndex,
"png_path" => $pngRelativePath // Just pass the relative file path!
],
"status" => "pending",
"time" => time()
];
enqueueJob($job);
// Immediately respond to frontend that the job is safely queued
$response = array("status" => "success", "chat_id" => $chatId);
} else {
$response = array("status" => "error", "message" => "Source image file not found on disk.");
}
} else {
$response = array("status" => "error", "message" => "Invalid message index or target is not an image.");
}
} else {
$response = array("status" => "error", "message" => "Chat file not found.");
}
break;
case "upscale_debug":
$chList = curl_init('http://'.$BACKEND_IP.':'.$BACKEND_IMG_PORT.'/sdapi/v1/upscalers');
curl_setopt($chList, CURLOPT_TIMEOUT, $curl_timeout);
curl_setopt($chList, CURLOPT_CONNECTTIMEOUT, $curl_connout);
curl_setopt($chList, CURLOPT_RETURNTRANSFER, true);
$upscalersRes = curl_exec($chList);
curl_close($chList);
// DEBUG: Log all available upscaler names to disk so you can see exact strings
file_put_contents(__DIR__ . '/upscalers_debug.json', $upscalersRes);
break;
case "check_queue_status":
triggerQueueRunner();
$queueFile = __DIR__ . '/queue.json';
$queueData = file_exists($queueFile) ? file_get_contents($queueFile) : "[]";
$activeQueue = json_decode($queueData,true);
// Optional cleanup: purge completed jobs older than 10 seconds or clean specific chat
$chatIdFilter = preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['chat_id'] ?? '');
if (!empty($chatIdFilter)) {
$activeQueue = array_values(array_filter($activeQueue, function($j) use ($chatIdFilter) {
// Keep if not completed for this chat, or keep other chats' jobs
return !($j['chat_id'] === $chatIdFilter && $j['status'] === 'completed');
}));
file_put_contents($queueFile, json_encode($activeQueue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX);
}
$response = array("status" => "success", "queue" => json_decode($queueData));
break;
}
echo json_encode($response);
exit;
function enqueueJob($job) {
$queueFile = __DIR__ . '/queue.json';
$queue = file_exists($queueFile) ? json_decode(file_get_contents($queueFile), true) ?: [] : [];
$queue[] = $job;
file_put_contents($queueFile, json_encode($queue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX);
triggerQueueRunner();
}
function triggerQueueRunner() {
global $php_bin; // setup.php
$lockFile = __DIR__ . '/worker.lock';
$fp = fopen($lockFile, 'c+');
// Check if worker lock is already held without blocking
if (flock($fp, LOCK_EX | LOCK_NB)) {
flock($fp, LOCK_UN);
fclose($fp);
$workerScript = __DIR__ . '/worker.php';
if (strncasecmp(PHP_OS, 'WIN', 3) == 0) {
//--pclose(popen("start /B php " . escapeshellarg($workerScript) . " > NUL 2>&1", "r"));
// PHP_BINARY points directly to C:\xampp\php\php.exe
$phpPath = escapeshellarg($php_bin);
$scriptPath = escapeshellarg($workerScript);
pclose(popen("cmd /c \"start \"\" /B {$phpPath} {$scriptPath} > NUL 2>&1\"", "r"));
} else {
exec("php " . escapeshellarg($workerScript) . " > /dev/null 2>&1 &");
}
} else {
fclose($fp); // Already running, don't spawn a duplicate
}
}
function updateChatStatus($chatId, $status) {
global $indexFile;
if (!file_exists($indexFile)) return;
$index = json_decode(file_get_contents($indexFile), true) ?: [];
foreach ($index as &$item) {
if ($item['id'] === $chatId) {
$item['status'] = $status;
$item['time'] = time();
break;
}
}
file_put_contents($indexFile, json_encode($index, JSON_PRETTY_PRINT));
}
?>
Top