Autosave: 20260310-054458
This commit is contained in:
parent
ce9f0dfe67
commit
5bb874b992
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
493
ai/LocalAIApi.php
Normal file
493
ai/LocalAIApi.php
Normal file
@ -0,0 +1,493 @@
|
||||
<?php
|
||||
// LocalAIApi — proxy client for the Responses API.
|
||||
// Usage (async: auto-polls status until ready):
|
||||
// require_once __DIR__ . '/ai/LocalAIApi.php';
|
||||
// $response = LocalAIApi::createResponse([
|
||||
// 'input' => [
|
||||
// ['role' => 'system', 'content' => 'You are a helpful assistant.'],
|
||||
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
|
||||
// ],
|
||||
// ]);
|
||||
// if (!empty($response['success'])) {
|
||||
// // response['data'] contains full payload, e.g.:
|
||||
// // {
|
||||
// // "id": "resp_xxx",
|
||||
// // "status": "completed",
|
||||
// // "output": [
|
||||
// // {"type": "reasoning", "summary": []},
|
||||
// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
|
||||
// // ]
|
||||
// // }
|
||||
// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...)
|
||||
// }
|
||||
// Poll settings override:
|
||||
// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]);
|
||||
|
||||
class LocalAIApi
|
||||
{
|
||||
/** @var array<string,mixed>|null */
|
||||
private static ?array $configCache = null;
|
||||
|
||||
/**
|
||||
* Signature compatible with the OpenAI Responses API.
|
||||
*
|
||||
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
|
||||
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid).
|
||||
* @return array{
|
||||
* success:bool,
|
||||
* status?:int,
|
||||
* data?:mixed,
|
||||
* error?:string,
|
||||
* response?:mixed,
|
||||
* message?:string
|
||||
* }
|
||||
*/
|
||||
public static function createResponse(array $params, array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
$payload = $params;
|
||||
|
||||
if (empty($payload['input']) || !is_array($payload['input'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'input_missing',
|
||||
'message' => 'Parameter "input" is required and must be an array.',
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($payload['model']) || $payload['model'] === '') {
|
||||
$payload['model'] = $cfg['default_model'];
|
||||
}
|
||||
|
||||
$initial = self::request($options['path'] ?? null, $payload, $options);
|
||||
if (empty($initial['success'])) {
|
||||
return $initial;
|
||||
}
|
||||
|
||||
// Async flow: if backend returns ai_request_id, poll status until ready
|
||||
$data = $initial['data'] ?? null;
|
||||
if (is_array($data) && isset($data['ai_request_id'])) {
|
||||
$aiRequestId = $data['ai_request_id'];
|
||||
$pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds
|
||||
$pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds
|
||||
return self::awaitResponse($aiRequestId, [
|
||||
'timeout' => $pollTimeout,
|
||||
'interval' => $pollInterval,
|
||||
'headers' => $options['headers'] ?? [],
|
||||
'timeout_per_call' => $options['timeout'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return $initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snake_case alias for createResponse (matches the provided example).
|
||||
*
|
||||
* @param array<string,mixed> $params
|
||||
* @param array<string,mixed> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function create_response(array $params, array $options = []): array
|
||||
{
|
||||
return self::createResponse($params, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a raw request to the AI proxy.
|
||||
*
|
||||
* @param string $path Endpoint (may be an absolute URL).
|
||||
* @param array<string,mixed> $payload JSON payload.
|
||||
* @param array<string,mixed> $options Additional request options.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function request(?string $path = null, array $payload = [], array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
|
||||
$projectUuid = $cfg['project_uuid'];
|
||||
if (empty($projectUuid)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'project_uuid_missing',
|
||||
'message' => 'PROJECT_UUID is not defined; aborting AI request.',
|
||||
];
|
||||
}
|
||||
|
||||
$defaultPath = $cfg['responses_path'] ?? null;
|
||||
$resolvedPath = $path ?? ($options['path'] ?? $defaultPath);
|
||||
if (empty($resolvedPath)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'project_id_missing',
|
||||
'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.',
|
||||
];
|
||||
}
|
||||
|
||||
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
|
||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||
if ($timeout <= 0) {
|
||||
$timeout = 30;
|
||||
}
|
||||
|
||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
||||
$verifyTls = array_key_exists('verify_tls', $options)
|
||||
? (bool) $options['verify_tls']
|
||||
: $baseVerifyTls;
|
||||
|
||||
$projectHeader = $cfg['project_header'];
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
$headers[] = $projectHeader . ': ' . $projectUuid;
|
||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||
foreach ($options['headers'] as $header) {
|
||||
if (is_string($header) && $header !== '') {
|
||||
$headers[] = $header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
|
||||
$payload['project_uuid'] = $projectUuid;
|
||||
}
|
||||
|
||||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
if ($body === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'json_encode_failed',
|
||||
'message' => 'Failed to encode request body to JSON.',
|
||||
];
|
||||
}
|
||||
|
||||
return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll AI request status until ready or timeout.
|
||||
*
|
||||
* @param int|string $aiRequestId
|
||||
* @param array<string,mixed> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function awaitResponse($aiRequestId, array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds
|
||||
$interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds
|
||||
if ($interval <= 0) {
|
||||
$interval = 5;
|
||||
}
|
||||
$perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null;
|
||||
|
||||
$deadline = time() + max($timeout, $interval);
|
||||
$headers = $options['headers'] ?? [];
|
||||
|
||||
while (true) {
|
||||
$statusResp = self::fetchStatus($aiRequestId, [
|
||||
'headers' => $headers,
|
||||
'timeout' => $perCallTimeout,
|
||||
]);
|
||||
if (!empty($statusResp['success'])) {
|
||||
$data = $statusResp['data'] ?? [];
|
||||
if (is_array($data)) {
|
||||
$statusValue = $data['status'] ?? null;
|
||||
if ($statusValue === 'success') {
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
'data' => $data['response'] ?? $data,
|
||||
];
|
||||
}
|
||||
if ($statusValue === 'failed') {
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return $statusResp;
|
||||
}
|
||||
|
||||
if (time() >= $deadline) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'timeout',
|
||||
'message' => 'Timed out waiting for AI response.',
|
||||
];
|
||||
}
|
||||
sleep($interval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch status for queued AI request.
|
||||
*
|
||||
* @param int|string $aiRequestId
|
||||
* @param array<string,mixed> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function fetchStatus($aiRequestId, array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
$projectUuid = $cfg['project_uuid'];
|
||||
if (empty($projectUuid)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'project_uuid_missing',
|
||||
'message' => 'PROJECT_UUID is not defined; aborting status check.',
|
||||
];
|
||||
}
|
||||
|
||||
$statusPath = self::resolveStatusPath($aiRequestId, $cfg);
|
||||
$url = self::buildUrl($statusPath, $cfg['base_url']);
|
||||
|
||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||
if ($timeout <= 0) {
|
||||
$timeout = 30;
|
||||
}
|
||||
|
||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
||||
$verifyTls = array_key_exists('verify_tls', $options)
|
||||
? (bool) $options['verify_tls']
|
||||
: $baseVerifyTls;
|
||||
|
||||
$projectHeader = $cfg['project_header'];
|
||||
$headers = [
|
||||
'Accept: application/json',
|
||||
$projectHeader . ': ' . $projectUuid,
|
||||
];
|
||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||
foreach ($options['headers'] as $header) {
|
||||
if (is_string($header) && $header !== '') {
|
||||
$headers[] = $header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract plain text from a Responses API payload.
|
||||
*
|
||||
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request.
|
||||
* @return string
|
||||
*/
|
||||
public static function extractText(array $response): string
|
||||
{
|
||||
$payload = $response['data'] ?? $response;
|
||||
if (!is_array($payload)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!empty($payload['output']) && is_array($payload['output'])) {
|
||||
$combined = '';
|
||||
foreach ($payload['output'] as $item) {
|
||||
if (!isset($item['content']) || !is_array($item['content'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($item['content'] as $block) {
|
||||
if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) {
|
||||
$combined .= $block['text'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($combined !== '') {
|
||||
return $combined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($payload['choices'][0]['message']['content'])) {
|
||||
return (string) $payload['choices'][0]['message']['content'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to decode JSON emitted by the model (handles markdown fences).
|
||||
*
|
||||
* @param array<string,mixed> $response
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public static function decodeJsonFromResponse(array $response): ?array
|
||||
{
|
||||
$text = self::extractText($response);
|
||||
if ($text === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode($text, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
$stripped = preg_replace('/^```json|```$/m', '', trim($text));
|
||||
if ($stripped !== null && $stripped !== $text) {
|
||||
$decoded = json_decode($stripped, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from ai/config.php.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function config(): array
|
||||
{
|
||||
if (self::$configCache === null) {
|
||||
$configPath = __DIR__ . '/config.php';
|
||||
if (!file_exists($configPath)) {
|
||||
throw new RuntimeException('AI config file not found: ai/config.php');
|
||||
}
|
||||
$cfg = require $configPath;
|
||||
if (!is_array($cfg)) {
|
||||
throw new RuntimeException('Invalid AI config format: expected array');
|
||||
}
|
||||
self::$configCache = $cfg;
|
||||
}
|
||||
|
||||
return self::$configCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an absolute URL from base_url and a path.
|
||||
*/
|
||||
private static function buildUrl(string $path, string $baseUrl): string
|
||||
{
|
||||
$trimmed = trim($path);
|
||||
if ($trimmed === '') {
|
||||
return $baseUrl;
|
||||
}
|
||||
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
|
||||
return $trimmed;
|
||||
}
|
||||
if ($trimmed[0] === '/') {
|
||||
return $baseUrl . $trimmed;
|
||||
}
|
||||
return $baseUrl . '/' . $trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve status path based on configured responses_path and ai_request_id.
|
||||
*
|
||||
* @param int|string $aiRequestId
|
||||
* @param array<string,mixed> $cfg
|
||||
* @return string
|
||||
*/
|
||||
private static function resolveStatusPath($aiRequestId, array $cfg): string
|
||||
{
|
||||
$basePath = $cfg['responses_path'] ?? '';
|
||||
$trimmed = rtrim($basePath, '/');
|
||||
if ($trimmed === '') {
|
||||
return '/ai-request/' . rawurlencode((string)$aiRequestId) . '/status';
|
||||
}
|
||||
if (substr($trimmed, -11) !== '/ai-request') {
|
||||
$trimmed .= '/ai-request';
|
||||
}
|
||||
return $trimmed . '/' . rawurlencode((string)$aiRequestId) . '/status';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared CURL sender for GET/POST requests.
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param string|null $body
|
||||
* @param array<int,string> $headers
|
||||
* @param int $timeout
|
||||
* @param bool $verifyTls
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array
|
||||
{
|
||||
if (!function_exists('curl_init')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'curl_missing',
|
||||
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
|
||||
];
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
||||
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
||||
|
||||
$upper = strtoupper($method);
|
||||
if ($upper === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? '');
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
||||
}
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
if ($responseBody === false) {
|
||||
$error = curl_error($ch) ?: 'Unknown cURL error';
|
||||
curl_close($ch);
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'curl_error',
|
||||
'message' => $error,
|
||||
];
|
||||
}
|
||||
|
||||
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$decoded = null;
|
||||
if ($responseBody !== '' && $responseBody !== null) {
|
||||
$decoded = json_decode($responseBody, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$decoded = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($status >= 200 && $status < 300) {
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => $status,
|
||||
'data' => $decoded ?? $responseBody,
|
||||
];
|
||||
}
|
||||
|
||||
$errorMessage = 'AI proxy request failed';
|
||||
if (is_array($decoded)) {
|
||||
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage;
|
||||
} elseif (is_string($responseBody) && $responseBody !== '') {
|
||||
$errorMessage = $responseBody;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => $status,
|
||||
'error' => $errorMessage,
|
||||
'response' => $decoded ?? $responseBody,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy alias for backward compatibility with the previous class name.
|
||||
if (!class_exists('OpenAIService')) {
|
||||
class_alias(LocalAIApi::class, 'OpenAIService');
|
||||
}
|
||||
52
ai/config.php
Normal file
52
ai/config.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
// OpenAI proxy configuration (workspace scope).
|
||||
// Reads values from environment variables or executor/.env.
|
||||
|
||||
$projectUuid = getenv('PROJECT_UUID');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
|
||||
if (
|
||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
||||
($projectId === false || $projectId === null || $projectId === '')
|
||||
) {
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$value = trim($value, "\"' ");
|
||||
if (getenv($key) === false || getenv($key) === '') {
|
||||
putenv("{$key}={$value}");
|
||||
}
|
||||
}
|
||||
$projectUuid = getenv('PROJECT_UUID');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
}
|
||||
}
|
||||
|
||||
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
||||
$projectId = ($projectId === false) ? null : $projectId;
|
||||
|
||||
$baseUrl = 'https://flatlogic.com';
|
||||
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
||||
|
||||
return [
|
||||
'base_url' => $baseUrl,
|
||||
'responses_path' => $responsesPath,
|
||||
'project_id' => $projectId,
|
||||
'project_uuid' => $projectUuid,
|
||||
'project_header' => 'project-uuid',
|
||||
'default_model' => 'gpt-5-mini',
|
||||
'timeout' => 30,
|
||||
'verify_tls' => true,
|
||||
];
|
||||
64
api/chat.php
Normal file
64
api/chat.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$message = $input['message'] ?? '';
|
||||
|
||||
if (empty($message)) {
|
||||
echo json_encode(['reply' => "I didn't catch that. Could you repeat?"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Fetch Knowledge Base (FAQs)
|
||||
$stmt = db()->query("SELECT keywords, answer FROM faqs");
|
||||
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
|
||||
foreach ($faqs as $faq) {
|
||||
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
|
||||
}
|
||||
|
||||
// 2. Construct Prompt for AI
|
||||
$systemPrompt = "You are a helpful, friendly AI assistant for this website. " .
|
||||
"Use the provided Knowledge Base to answer user questions accurately. " .
|
||||
"If the answer is found in the Knowledge Base, rephrase it naturally. " .
|
||||
"If the answer is NOT in the Knowledge Base, use your general knowledge to help, " .
|
||||
"but politely mention that you don't have specific information about that if it seems like a site-specific question. " .
|
||||
"Keep answers concise and professional.\n\n" .
|
||||
$knowledgeBase;
|
||||
|
||||
// 3. Call AI API
|
||||
$response = LocalAIApi::createResponse([
|
||||
'model' => 'gpt-4o-mini',
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $message],
|
||||
]
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$aiReply = LocalAIApi::extractText($response);
|
||||
|
||||
// 4. Save to Database
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
|
||||
$stmt->execute([$message, $aiReply]);
|
||||
} catch (Exception $e) {
|
||||
error_log("DB Save Error: " . $e->getMessage());
|
||||
// Continue even if save fails, so the user still gets a reply
|
||||
}
|
||||
|
||||
echo json_encode(['reply' => $aiReply]);
|
||||
} else {
|
||||
// Fallback if AI fails
|
||||
error_log("AI Error: " . ($response['error'] ?? 'Unknown'));
|
||||
echo json_encode(['reply' => "I'm having trouble connecting to my brain right now. Please try again later."]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Chat Error: " . $e->getMessage());
|
||||
echo json_encode(['reply' => "An internal error occurred."]);
|
||||
}
|
||||
91
api/telegram_webhook.php
Normal file
91
api/telegram_webhook.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
// Get Telegram Update
|
||||
$content = file_get_contents("php://input");
|
||||
$update = json_decode($content, true);
|
||||
|
||||
if (!$update || !isset($update['message'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$message = $update['message'];
|
||||
$chatId = $message['chat']['id'];
|
||||
$text = $message['text'] ?? '';
|
||||
|
||||
if (empty($text)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get Telegram Token from DB
|
||||
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
|
||||
$token = $stmt->fetchColumn();
|
||||
|
||||
if (!$token) {
|
||||
error_log("Telegram Error: No bot token found in settings.");
|
||||
exit;
|
||||
}
|
||||
|
||||
function sendTelegramMessage($chatId, $text, $token) {
|
||||
$url = "https://api.telegram.org/bot$token/sendMessage";
|
||||
$data = [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'parse_mode' => 'Markdown'
|
||||
];
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => http_build_query($data),
|
||||
],
|
||||
];
|
||||
$context = stream_context_create($options);
|
||||
return file_get_contents($url, false, $context);
|
||||
}
|
||||
|
||||
// Process with AI (Similar logic to api/chat.php)
|
||||
try {
|
||||
// 1. Fetch Knowledge Base
|
||||
$stmt = db()->query("SELECT keywords, answer FROM faqs");
|
||||
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
|
||||
foreach ($faqs as $faq) {
|
||||
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
|
||||
}
|
||||
|
||||
$systemPrompt = "You are a helpful AI assistant integrated with Telegram. " .
|
||||
"Use the provided Knowledge Base to answer user questions. " .
|
||||
"Keep answers concise for mobile reading. Use Markdown for formatting.\n\n" .
|
||||
$knowledgeBase;
|
||||
|
||||
// 2. Call AI
|
||||
$response = LocalAIApi::createResponse([
|
||||
'model' => 'gpt-4o-mini',
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $text],
|
||||
]
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$aiReply = LocalAIApi::extractText($response);
|
||||
|
||||
// 3. Save History
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
|
||||
$stmt->execute(["[Telegram] " . $text, $aiReply]);
|
||||
} catch (Exception $e) {}
|
||||
|
||||
// 4. Send back to Telegram
|
||||
sendTelegramMessage($chatId, $aiReply, $token);
|
||||
} else {
|
||||
sendTelegramMessage($chatId, "I'm sorry, I encountered an error processing your request.", $token);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Telegram Webhook Error: " . $e->getMessage());
|
||||
}
|
||||
82
assets/css/custom.css
Normal file
82
assets/css/custom.css
Normal file
@ -0,0 +1,82 @@
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-dark: #4f46e5;
|
||||
--accent: #d946ef;
|
||||
--dark-bg: #1e1b4b;
|
||||
--card-bg: rgba(30, 27, 75, 0.9);
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--gradient: linear-gradient(135deg, #6366f1 0%, #d946ef 100%);
|
||||
--bg-gradient: linear-gradient(-45deg, #1e1b4b, #312e81, #1e1b4b);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-gradient);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: var(--text-main);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Card Styling (AppWizzy style) */
|
||||
.chat-container, .admin-container {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(20px);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
background: var(--gradient);
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
input, .form-control {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
}
|
||||
39
assets/js/main.js
Normal file
39
assets/js/main.js
Normal file
@ -0,0 +1,39 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
});
|
||||
});
|
||||
BIN
assets/pasted-20260310-052744-6ae52ecd.png
Normal file
BIN
assets/pasted-20260310-052744-6ae52ecd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/pasted-20260310-052911-0f126184.jpg
Normal file
BIN
assets/pasted-20260310-052911-0f126184.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
BIN
assets/vm-shot-2026-03-10T05-29-08-831Z.jpg
Normal file
BIN
assets/vm-shot-2026-03-10T05-29-08-831Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
Loading…
x
Reference in New Issue
Block a user