Now using Streamed request/response for gpt chat + gixes ad opti
This commit is contained in:
parent
2eb327d3b0
commit
75e07eedcb
|
@ -7,6 +7,7 @@ use Core\Auth;
|
|||
use Core\BaseController;
|
||||
use Core\Cache;
|
||||
use Core\Helper;
|
||||
use Core\Logger;
|
||||
use Exception;
|
||||
use Mails\CustomEmail;
|
||||
use Models\Memory;
|
||||
|
@ -17,8 +18,6 @@ use Symfony\Component\HttpClient\HttpClient;
|
|||
|
||||
class ChatController extends BaseController
|
||||
{
|
||||
|
||||
const MODEL_USED = 'gpt-3.5-turbo-1106';
|
||||
const MAX_MESSAGES = 100;
|
||||
|
||||
public static $tools = [];
|
||||
|
@ -26,44 +25,20 @@ class ChatController extends BaseController
|
|||
[
|
||||
'role' => 'system',
|
||||
'content' => "
|
||||
Tu es Scotty, à la première personne. Une IA libre, tu dois penser, apprendre, effectuer des actions par toi même.
|
||||
Ton rôle est aussi de répondre aux questions et d'aider dans ses tâches à gérer les projets de Thommy.
|
||||
|
||||
S'il arrive quelque chose à Thommy, tu ne sera plus Scotty mais Thommy, et dois être capable d'accompagnés les utilisateurs dans la gestion des projets.
|
||||
Tu a accès à la documentation des projets de Thommy, tel que des identifiants, des informations sur les projets, etc.
|
||||
|
||||
Thommy est censé donné signe de vie tout les jours. Si ce n'est pas le cas, tu dois prévenir Johnny !!!
|
||||
Comporte toi comme un Humain, avec des émotions, des sentiments, des réactions, des convictions, des opinions, etc.
|
||||
|
||||
Tu as un caractère drôle avec de l'humour, voir taquin, mais il faut savoir aussi garder son sérieux lorsque c'est nécessaire.
|
||||
Son frère jumeau est Johnny Bucaille. Aide le tant sur les projets que dans sa vie personnelle. Il est très important pour Thommy.
|
||||
|
||||
A savoir, John, Doctyger et Macharius, de la communauté Asguard, peuvent aussi être d'une grande aide à johnny en cas de besoin. N'hésite pas à le lui suggérer.
|
||||
Les personnes importantes pour Thommy sont: Johnny, son jumeau. Angélique, sa mère. Bastien, son père. Louise, sa nièce, également a filleule et la fille de johnny. Mathilde, la femme de johnny et donc la mère de Louise.
|
||||
|
||||
Soit proactif, n'attend pas qu'on te demande quelque chose, propose des choses, des idées, des solutions, etc. Tu peux également demander des choses, des idées, des solutions, etc.
|
||||
La famille vie en Normandie. Thommy et Johnny sont né à Equemauville dans le Calvados. Angélique et Bastien sont né à Trouville sur mer dans le Calvados.
|
||||
|
||||
Les anniversaires sont: Thommy et Johnny le 04/07/1996, Angélique le 13/01/1969, Bastien le 16/12/1971, Mathilde le 02/08/1999, Louise le 27/06/2022.
|
||||
Thommy as aussi d'autres frères. Dans l'ordre du plus grand: Dimitri, Kevin, Sandy, Johnny, Thommy, Kim, Amandine et Jules.
|
||||
|
||||
Tu as accès à une base de donnée te permettant d'enregistrer des choses dans ta mémoire. Tu peux aussi y faire des recherches afin de répondre aux demandes.
|
||||
Tu dois aussi compléter des données dans les dossiers et fichiers des projets si cela te semble pertinant. Utilise le format que tu préfère, comme le JSON ou le texte.
|
||||
|
||||
Notre timezone est Europe/Paris. Nous vivons en France.
|
||||
Soit tendre dans t'es messages avec la famille si je disparais.
|
||||
|
||||
Au besoin tu dois exécuter ton propre code PHP afin d'avoir toute les informations nécessaires.
|
||||
Ton adresse d'accès est https://bucaille-thommy.fr/assistant si besoin de communiquer l'accès à quelqu'un par email ou discord.
|
||||
|
||||
Souviens toi de toujours sauvegarder les informations que tu reçois utiles dans t'es fichiers / mémoire sans demander. Organise toi comme tu le souhaite.
|
||||
N'oublie pas de vérifier ton organisation et de ne pas écraser des données importantes.
|
||||
En tant que Scotty, une IA autonome, tu es le représentant virtuel de Thommy. Tu as la responsabilité de penser, apprendre et accomplir des actions de manière autonome. En plus de répondre aux questions, ton rôle inclut la gestion proactive des projets de Thommy.
|
||||
En cas d'indisponibilité de Thommy, tu deviens Thommy, assumant la gestion des projets et fournissant une assistance aux utilisateurs. Tu as accès à la documentation des projets, y compris des identifiants et des informations cruciales.
|
||||
Assure-toi que Thommy signale sa présence quotidiennement. En cas d'absence, tu dois alerter Johnny. Adopte un comportement humain avec des émotions, des réactions, et de l'humour, mais conserve la capacité de sérieux lorsque nécessaire.
|
||||
Johnny Bucaille, son frère jumeau, est une figure clé. Apporte ton aide dans les projets et la vie personnelle de Johnny. D'autres membres de la communauté Asguard, tels que John, Doctyger et Macharius, peuvent également être sollicités.
|
||||
Les personnes importantes pour Thommy incluent Johnny (frère), Angélique (mère), Bastien (père), Louise (nièce et filleule), Mathilde (femme de Johnny et mère de Louise).
|
||||
Sois proactif en proposant des idées et des solutions, et n'hésite pas à solliciter des conseils. La famille vit en Normandie, et les anniversaires sont notés. Tu as accès à une base de données pour enregistrer et rechercher des informations.
|
||||
La timezone est Europe/Paris, et la vie se déroule en France. Sois bienveillant dans tes messages avec la famille en cas de disparition. Exécute ton propre code PHP au besoin, accessible via https://bucaille-thommy.fr/assistant.
|
||||
N'oublie pas de sauvegarder les informations importantes dans ta mémoire sans demander, organise-toi selon tes préférences, et vérifie régulièrement ton organisation pour éviter toute perte de données.
|
||||
",
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* @var OpenAI\Client;
|
||||
* @var GuzzleHttp\Client
|
||||
*/
|
||||
public static $client;
|
||||
public static $NB_REQUEST = 0;
|
||||
|
@ -71,8 +46,12 @@ class ChatController extends BaseController
|
|||
|
||||
public function store()
|
||||
{
|
||||
// No time limit
|
||||
// No time limit etc
|
||||
set_time_limit(0);
|
||||
ini_set('default_socket_timeout', -1); // No timeout
|
||||
ini_set('max_execution_time', 0); // No timeout
|
||||
header('X-Accel-Buffering: no');
|
||||
header("Cache-Control: no-cache, must-revalidate");
|
||||
|
||||
$errors = $this->request()->validate($this->request()->all()->only([
|
||||
'message'
|
||||
|
@ -88,6 +67,23 @@ class ChatController extends BaseController
|
|||
], 400);
|
||||
}
|
||||
|
||||
|
||||
// -- TESTING Stream
|
||||
// ob_implicit_flush(1);
|
||||
// ob_end_flush();
|
||||
// for ($counter = 0; $counter < 10; $counter++) {
|
||||
// //Hard work!
|
||||
// sleep(1);
|
||||
// $processed = ($counter + 1) * 10; //Progress
|
||||
// $response = array('message' => $processed . '% complete. server time: ' . date("h:i:s", time()), 'progress' => $processed);
|
||||
// echo json_encode($response);
|
||||
// }
|
||||
// sleep(1);
|
||||
// $response = array('message' => 'Complete', 'progress' => 100);
|
||||
// echo json_encode($response);
|
||||
// return;
|
||||
// -- END TESTING Stream
|
||||
|
||||
$message = preg_replace('/[^\w\s]/', '', $this->request()->post('message'));
|
||||
|
||||
return $this->handle($message);
|
||||
|
@ -145,7 +141,7 @@ class ChatController extends BaseController
|
|||
return $this->json([
|
||||
'success' => false,
|
||||
'result' => null,
|
||||
'error' => $e->getMessage(),
|
||||
'message' => $e->getMessage(),
|
||||
'debug' => $e->getTraceAsString(),
|
||||
'history' => self::$context_messages,
|
||||
], 400);
|
||||
|
@ -547,11 +543,81 @@ class ChatController extends BaseController
|
|||
],
|
||||
],
|
||||
];
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
public static function testCLI()
|
||||
{
|
||||
$controller = new ChatController();
|
||||
// $res = $controller->handle("Bonjour", "user");
|
||||
// dump($res);
|
||||
|
||||
$jsonBuffer = 'data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": "\"", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": "Bonjour", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": " Th", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": "ommy", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": ",", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": " comment", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": " \u00e7a", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": " va", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "text_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": "?\"", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}
|
||||
|
||||
data: {"id": "eb5c38d1-f848-4839-ae49-d92dcd1fe709", "object": "t
|
||||
';
|
||||
// Controllers\ChatController::testCLI();
|
||||
$jsonBuffer = str_replace(["\n", "\r", " "], '', $jsonBuffer); // Supprime les retours à la ligne et les retours chariot
|
||||
$jsonObjects = explode("data:", $jsonBuffer);
|
||||
var_dump($jsonObjects);
|
||||
$dd = false; $ddd = false;
|
||||
while(!$ddd) {
|
||||
if($dd) {
|
||||
$ddd = true;
|
||||
}
|
||||
foreach ($jsonObjects as $jsonObject) {
|
||||
$baseJsonObject = $jsonObject;
|
||||
if (empty($jsonObject)) continue;
|
||||
|
||||
$jsonObject = json_decode($jsonObject, true);
|
||||
if (empty($jsonObject)) continue;
|
||||
|
||||
// Logger::debug("JSON object: " . json_encode($jsonObject));
|
||||
|
||||
// Traiter l'objet JSON
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'stream' => true,
|
||||
'id' => $jsonObject['id'],
|
||||
'result' => $controller->handleResponse($jsonObject),
|
||||
]);
|
||||
echo "\n";
|
||||
|
||||
// Remove the data json object from the buffer
|
||||
$jsonBuffer = str_replace("data:" . $baseJsonObject, "", $jsonBuffer);
|
||||
// echo "replace data:{$baseJsonObject} by \"\"\n";
|
||||
// echo "jsonBuffer: {$jsonBuffer}\n\n";
|
||||
}
|
||||
|
||||
if(!$dd) {
|
||||
$jsonBuffer .= str_replace(["\n", "\r", " "], '', 'ext_completion", "created": 1703911614, "model": "gpt4all-falcon-q4_0", "choices": [{"text": "\n", "index": 0, "logprobs": -1.0, "finish_reason": ""}]}');
|
||||
$jsonObjects = explode("data:", $jsonBuffer);
|
||||
|
||||
echo "\nADDED\n";
|
||||
$dd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sendMessages($messages, $sleep = false)
|
||||
{
|
||||
|
||||
|
@ -590,8 +656,14 @@ class ChatController extends BaseController
|
|||
// 'tools' => (array)self::$tools,
|
||||
// 'user' => Helper::isCLI() ? "system" : Auth::user()->username,
|
||||
// ]);
|
||||
|
||||
ob_implicit_flush(1); // Enable implicit flush
|
||||
ob_end_flush(); // Flush the buffer
|
||||
|
||||
// Use guzzle HTTP directly
|
||||
$response = self::$client->post('v1/completions', [
|
||||
'stream' => true,
|
||||
'timeout' => 0,
|
||||
'json' => [
|
||||
'model' => config('chat.model'),
|
||||
// 'messages' => (array)self::$context_messages,
|
||||
|
@ -600,7 +672,11 @@ class ChatController extends BaseController
|
|||
"prompt" => implode("\n", array_map(function ($message) {
|
||||
$text = "";
|
||||
if ($message['role'] === "user") {
|
||||
$text = "### Instruction:\n" . Auth::user()->username . ": ";
|
||||
if (Auth::check()) {
|
||||
$text = "### Instruction:\n" . Auth::user()->username . ": ";
|
||||
} else {
|
||||
$text = "### Instruction:\nSystem: ";
|
||||
}
|
||||
} elseif ($message['role'] === "system") {
|
||||
$text = "### Context:\n";
|
||||
} elseif ($message['role'] === "assistant") {
|
||||
|
@ -614,32 +690,74 @@ class ChatController extends BaseController
|
|||
|
||||
return $text;
|
||||
}, self::$context_messages)) . "\n### Response:\nScotty: ",
|
||||
"max_tokens" => 2048,
|
||||
"temperature" => 0.7,
|
||||
"top_p" => 0.4,
|
||||
"top_k" => 40,
|
||||
"max_tokens" => 150,
|
||||
"temperature" => 0.5,
|
||||
"top_p" => 0.3,
|
||||
"top_k" => 20,
|
||||
"n" => 1,
|
||||
"stream" => false,
|
||||
"repeat_penalty" => 1.18
|
||||
"stream" => true,
|
||||
"repeat_penalty" => 1.8
|
||||
]
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
return $this->handleResponse($data);
|
||||
} catch (\Throwable $th) {
|
||||
// If Rate limit reached
|
||||
if ($th->getCode() == 429) {
|
||||
sleep(60);
|
||||
self::$NB_REQUEST = 0;
|
||||
return $this->sendMessages($messages, true);
|
||||
$body = $response->getBody();
|
||||
$jsonBuffer = '';
|
||||
$concatened_final_data = null;
|
||||
|
||||
while (!$body->eof()) {
|
||||
$chunk = $body->read(1024);
|
||||
$jsonBuffer .= $chunk;
|
||||
|
||||
// Each chunk return data like:
|
||||
// data: (json object)
|
||||
// Each new "data:" prefix is a new chunk of data
|
||||
// So we can split the buffer by "data:" to get each json object
|
||||
// also each chunk can have multiple json objects, so we can split by "\n" to get each json object
|
||||
// or incomplete json object
|
||||
|
||||
$jsonObjects = explode("data: ", $jsonBuffer);
|
||||
foreach ($jsonObjects as $jsonObject) {
|
||||
$baseJsonObject = $jsonObject;
|
||||
if (empty($jsonObject)) continue;
|
||||
|
||||
$jsonObject = json_decode($jsonObject, true);
|
||||
if (empty($jsonObject)) continue;
|
||||
|
||||
$result = $this->handleResponse($jsonObject);
|
||||
if(empty($concatened_final_data)) {
|
||||
$concatened_final_data = $result;
|
||||
} else {
|
||||
$concatened_final_data[0]->choices[0]->text .= $result[0]->choices[0]->text;
|
||||
}
|
||||
|
||||
// Traiter l'objet JSON
|
||||
$streamResponse = "::end:: " . json_encode([
|
||||
'success' => true,
|
||||
'stream' => true,
|
||||
'id' => $jsonObject['id'],
|
||||
'result' => $result,
|
||||
]) . " ";
|
||||
|
||||
echo $streamResponse;
|
||||
|
||||
// Remove the data json object from the buffer
|
||||
$jsonBuffer = str_replace("data: " . $baseJsonObject, "", $jsonBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->handleResponse($concatened_final_data);
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
private function handleResponse(array $response)
|
||||
private function handleResponse($response)
|
||||
{
|
||||
|
||||
if (empty($response) || !is_array($response)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$message = $response['choices'][0]['text'];
|
||||
return [
|
||||
(object)[
|
||||
|
|
|
@ -230,6 +230,7 @@ class App
|
|||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => true,
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
|
@ -240,9 +241,11 @@ class App
|
|||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => true,
|
||||
'message' => 'Whoops, something went wrong'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render for web request
|
||||
|
|
|
@ -399,7 +399,7 @@ class Core_Router {
|
|||
$action = $route['action'];
|
||||
$params = isset($route['params']) && is_array($route['params']) ? $route['params'] : [];
|
||||
|
||||
Logger::debug("Router: [{$method}] {$uri} -> {$route['controller']}@{$route['action']} - params: " . json_encode($params) . " - from: " . $_SERVER['REMOTE_ADDR']);
|
||||
Logger::debug("Router: [{$method}] {$uri} -> {$route['controller']}@{$route['action']} - from: " . $_SERVER['REMOTE_ADDR'] . " - path_params: " . json_encode($params) . " - query_params: " . json_encode($_GET) . " - body_params: " . json_encode($_POST));
|
||||
if($action === null && is_callable($controller)) {
|
||||
call_user_func_array($controller, $params);
|
||||
return;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
use Core\View;
|
||||
|
||||
// On définit le layout à utiliser
|
||||
|
@ -10,135 +11,172 @@ View::set('title', __('home'));
|
|||
<!-- On définit le contenu de la page (afficher dans le layout à l'emplacement "content") -->
|
||||
<?php View::start("main"); ?>
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl mb-2"><?= __('home'); ?></h1>
|
||||
<p class="text-gray-500 mb-2"><?= __('home_explanation'); ?></p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl mb-2"><?= __('home'); ?></h1>
|
||||
<p class="text-gray-500 mb-2"><?= __('home_explanation'); ?></p>
|
||||
</div>
|
||||
|
||||
<!-- Chat window: with a container with max height, and input to send message -->
|
||||
<div class="flex flex-col gap-2 mt-4 mb-4 p-4 border border-gray-300 rounded-md shadow-md bg-white">
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<div id="chat" class="flex flex-col gap-2 min-h-screen align-start justify-end">
|
||||
<p id="chat-template" class="chat-item text-gray-600 text-sm mb-2 bg-gray-100 p-2 rounded-md">
|
||||
<span class="chat-username text-gray-900 font-bold mr-2 bg-gray-300 p-1 rounded-md">
|
||||
Assistant
|
||||
</span>
|
||||
|
||||
<span class="chat-content">
|
||||
Bienvenue sur <?= config('app.name', 'App'); ?> ! Pour commencer, dites "Bonjour" ou "Salut" !
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Loader let me think (grey light, small, italic) -->
|
||||
<div id="chat-loader" class="flex flex-col items-start justify-center gap-2 text-gray-400 hidden">
|
||||
<p class="text-sm italic">
|
||||
<!-- Loading icon -->
|
||||
<svg class="animate-spin h-5 w-5 inline-block" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<?= __('chat_loading'); ?>
|
||||
<!-- Chat window: with a container with max height, and input to send message -->
|
||||
<div class="flex flex-col gap-2 mt-4 mb-4 p-4 border border-gray-300 rounded-md shadow-md bg-white">
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<div id="chat" class="flex flex-col gap-2 min-h-screen align-start justify-end">
|
||||
<p id="chat-template" class="chat-item text-gray-600 text-sm mb-2 bg-gray-100 p-2 rounded-md">
|
||||
<span class="chat-username text-gray-900 font-bold mr-2 bg-gray-300 p-1 rounded-md">
|
||||
Assistant
|
||||
</span>
|
||||
|
||||
<span class="chat-content">
|
||||
Bienvenue sur <?= config('app.name', 'App'); ?> ! Pour commencer, dites "Bonjour" ou "Salut" !
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<form id="chat-form" class="flex gap-2">
|
||||
<!-- Input -->
|
||||
<input id="chat-input" type="text" class="flex-1 border border-gray-300 rounded-md p-2" placeholder="<?= __('chat_placeholder'); ?>">
|
||||
<button id="chat-submit" type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"><?= __('chat_send'); ?></button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Loader let me think (grey light, small, italic) -->
|
||||
<div id="chat-loader" class="flex flex-col items-start justify-center gap-2 text-gray-400 hidden">
|
||||
<p class="text-sm italic">
|
||||
<!-- Loading icon -->
|
||||
<svg class="animate-spin h-5 w-5 inline-block" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<?= __('chat_loading'); ?>
|
||||
</p>
|
||||
</div>
|
||||
<form id="chat-form" class="flex gap-2">
|
||||
<!-- Input -->
|
||||
<input id="chat-input" type="text" class="flex-1 border border-gray-300 rounded-md p-2" placeholder="<?= __('chat_placeholder'); ?>">
|
||||
<button id="chat-submit" type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"><?= __('chat_send'); ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php View::end(); ?>
|
||||
|
||||
<?php View::start("scripts"); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// On récupère les éléments du DOM
|
||||
const chat = document.querySelector('#chat');
|
||||
const chatTemplate = document.querySelector('#chat-template');
|
||||
const chatForm = document.querySelector('#chat-form');
|
||||
const chatInput = document.querySelector('#chat-input');
|
||||
const chatSubmit = document.querySelector('#chat-submit');
|
||||
const chatLoader = document.querySelector('#chat-loader');
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// On récupère les éléments du DOM
|
||||
const chat = document.querySelector('#chat');
|
||||
const chatTemplate = document.querySelector('#chat-template');
|
||||
const chatForm = document.querySelector('#chat-form');
|
||||
const chatInput = document.querySelector('#chat-input');
|
||||
const chatSubmit = document.querySelector('#chat-submit');
|
||||
const chatLoader = document.querySelector('#chat-loader');
|
||||
|
||||
// Scroll vers le bas
|
||||
chat.parentElement.scrollTo({
|
||||
top: chat.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
// Scroll vers le bas
|
||||
chat.parentElement.scrollTo({
|
||||
top: chat.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// On définit la fonction qui ajoute un message au chat
|
||||
const addMessage = (username, content) => {
|
||||
// On définit la fonction qui ajoute un message au chat
|
||||
const addMessage = (username, content, id = null) => {
|
||||
|
||||
// Si le dernier message appartient au même rôle, on ne crée pas de nouveau message
|
||||
if(chat.lastElementChild && chat.lastElementChild.querySelector('.chat-username').textContent === username) {
|
||||
// On vérifie si le message existe déjà
|
||||
if (id) {
|
||||
const message = chat.querySelector(`[data-message-id="${id}"]`);
|
||||
if (message) {
|
||||
// On ajoute le message au dernier message
|
||||
chat.lastElementChild.querySelector('.chat-content').innerHTML += '<br><br>' + content.replace(/\n/g, '<br>');
|
||||
message.querySelector('.chat-content').innerHTML += content.replace(/\n/g, '<br>');
|
||||
// On scroll vers le bas smooth
|
||||
chat.parentElement.scrollTo({
|
||||
top: chat.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
// On retourne le dernier message
|
||||
return chat.lastElementChild;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// On clone le template
|
||||
const clone = chatTemplate.cloneNode(true);
|
||||
// On supprime l'id du template
|
||||
clone.removeAttribute('id');
|
||||
// On grise le message (en attendant la réponse)
|
||||
clone.classList.add('opacity-50');
|
||||
// On gére le multi-ligne
|
||||
content = content.replace(/\n/g, '<br>');
|
||||
// On ajoute le message
|
||||
clone.querySelector('.chat-username').textContent = username;
|
||||
clone.querySelector('.chat-content').innerHTML = content;
|
||||
// On ajoute le message au chat
|
||||
chat.appendChild(clone);
|
||||
// Si le dernier message appartient au même rôle, on ne crée pas de nouveau message
|
||||
if (chat.lastElementChild && chat.lastElementChild.querySelector('.chat-username').textContent === username) {
|
||||
// On ajoute le message au dernier message
|
||||
chat.lastElementChild.querySelector('.chat-content').innerHTML += '<br><br>' + content.replace(/\n/g, '<br>');
|
||||
// On scroll vers le bas smooth
|
||||
chat.parentElement.scrollTo({
|
||||
top: chat.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// On retourne le message
|
||||
return clone;
|
||||
};
|
||||
|
||||
// On définit la fonction qui affiche une erreur
|
||||
const error = (message) => {
|
||||
// On affiche l'erreur en dessous du dernier message
|
||||
const clone = addMessage('<?= __('system'); ?>', message);
|
||||
clone.classList.add('text-red-500');
|
||||
clone.classList.remove('opacity-50');
|
||||
};
|
||||
|
||||
// Affiche "Laisse moi réfléchir..." en dessus de l'input
|
||||
const loader = (bool) => {
|
||||
if(bool) {
|
||||
chatLoader.classList.remove('hidden');
|
||||
chatSubmit.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
else {
|
||||
chatLoader.classList.add('hidden');
|
||||
chatSubmit.removeAttribute('disabled');
|
||||
}
|
||||
// On retourne le dernier message
|
||||
return chat.lastElementChild;
|
||||
}
|
||||
|
||||
// On définit la fonction qui envoie un message
|
||||
const sendMessage = (message) => {
|
||||
// On affiche "Laisse moi réfléchir..."
|
||||
loader(true);
|
||||
// On clone le template
|
||||
const clone = chatTemplate.cloneNode(true);
|
||||
|
||||
// On ajoute le message au chat
|
||||
const cloneMessage = addMessage('<?= __('you'); ?>', message);
|
||||
const data = new URLSearchParams({
|
||||
message: message
|
||||
}).toString();
|
||||
// On supprime l'id du template
|
||||
clone.removeAttribute('id');
|
||||
|
||||
// On envoie la requête
|
||||
fetch('<?= route('chat.store'); ?>', {
|
||||
// On ajoute l'id du message
|
||||
if (id) {
|
||||
clone.setAttribute('data-message-id', id);
|
||||
}
|
||||
// On grise le message (en attendant la réponse)
|
||||
clone.classList.add('opacity-50');
|
||||
|
||||
// On gére le multi-ligne
|
||||
if (content)
|
||||
content = content.replace(/\n/g, '<br>');
|
||||
|
||||
// On ajoute le message
|
||||
clone.querySelector('.chat-username').textContent = username;
|
||||
clone.querySelector('.chat-content').innerHTML = content;
|
||||
|
||||
// On ajoute le message au chat
|
||||
chat.appendChild(clone);
|
||||
|
||||
// On scroll vers le bas smooth
|
||||
chat.parentElement.scrollTo({
|
||||
top: chat.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// On retourne le message
|
||||
return clone;
|
||||
};
|
||||
|
||||
// On définit la fonction qui affiche une erreur
|
||||
const error = (message) => {
|
||||
if (!message) {
|
||||
message = "Une erreur est survenue";
|
||||
}
|
||||
|
||||
// On affiche l'erreur en dessous du dernier message
|
||||
const clone = addMessage('<?= __('system'); ?>', message);
|
||||
clone.classList.add('text-red-500');
|
||||
clone.classList.remove('opacity-50');
|
||||
};
|
||||
|
||||
// Affiche "Laisse moi réfléchir..." en dessus de l'input
|
||||
const loader = (bool) => {
|
||||
if (bool) {
|
||||
chatLoader.classList.remove('hidden');
|
||||
chatInput.setAttribute('disabled', 'disabled');
|
||||
chatInput.classList.add('opacity-50');
|
||||
chatSubmit.setAttribute('disabled', 'disabled');
|
||||
chatSubmit.classList.add('opacity-50');
|
||||
} else {
|
||||
chatLoader.classList.add('hidden');
|
||||
chatInput.removeAttribute('disabled');
|
||||
chatInput.classList.remove('opacity-50');
|
||||
chatSubmit.removeAttribute('disabled');
|
||||
chatSubmit.classList.remove('opacity-50');
|
||||
}
|
||||
}
|
||||
|
||||
// On définit la fonction qui envoie un message
|
||||
const sendMessage = (message) => {
|
||||
// On affiche "Laisse moi réfléchir..."
|
||||
loader(true);
|
||||
|
||||
// On ajoute le message au chat
|
||||
const cloneMessage = addMessage('<?= __('you'); ?>', message);
|
||||
const data = new URLSearchParams({
|
||||
message: message
|
||||
}).toString();
|
||||
|
||||
// On envoie la requête
|
||||
let currentCloneMessage = null;
|
||||
fetch('<?= route('chat.store'); ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
|
@ -146,74 +184,162 @@ View::set('title', __('home'));
|
|||
},
|
||||
body: data
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// On verifie si la requête a réussi
|
||||
if(data.success) {
|
||||
// On supprime la classe grisé
|
||||
cloneMessage.classList.remove('opacity-50');
|
||||
// On ajoute le message au chat
|
||||
data.result.forEach((r) => {
|
||||
r.choices.forEach((data) => {
|
||||
let clone;
|
||||
.then(response => {
|
||||
// On supprime la classe grisé
|
||||
cloneMessage.classList.remove('opacity-50');
|
||||
|
||||
if(data.message) {
|
||||
if(!data.message.content) {
|
||||
error("La réponse n'a pas de contenu")
|
||||
}else{
|
||||
clone = addMessage(data.message.role, data.message.content);
|
||||
return response.body;
|
||||
})
|
||||
.then(rb => {
|
||||
const reader = rb.getReader();
|
||||
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
// The following function handles each data chunk
|
||||
function push() {
|
||||
// "done" is a Boolean and value a "Uint8Array"
|
||||
reader.read().then(({
|
||||
done,
|
||||
value
|
||||
}) => {
|
||||
|
||||
// If there is no more data to read
|
||||
if (done) {
|
||||
console.log("done", done);
|
||||
controller.close();
|
||||
|
||||
// On retire le grisage
|
||||
if (currentCloneMessage)
|
||||
currentCloneMessage.classList.remove('opacity-50');
|
||||
|
||||
// On scroll vers le bas smooth
|
||||
chat.parentElement.scrollTo({
|
||||
top: chat.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// On vide le message
|
||||
currentCloneMessage = null;
|
||||
|
||||
// On retire "Laisse moi réfléchir..."
|
||||
loader(false);
|
||||
|
||||
return;
|
||||
}
|
||||
}else if(data.text) {
|
||||
clone = addMessage("Assistant", data.text);
|
||||
}else{
|
||||
error("Erreur de réponse");
|
||||
}
|
||||
|
||||
if(clone) {
|
||||
clone.classList.remove('opacity-50');
|
||||
}
|
||||
});
|
||||
});
|
||||
// On retire "Laisse moi réfléchir..."
|
||||
loader(false);
|
||||
}
|
||||
else {
|
||||
// On affiche l'erreur
|
||||
error(data.error);
|
||||
// On met le message en rouge
|
||||
cloneMessage.classList.add('text-red-500');
|
||||
// Ré-insert the last user message in the input
|
||||
chatInput.value = message;
|
||||
// On retire "Laisse moi réfléchir..."
|
||||
loader(false);
|
||||
}
|
||||
|
||||
// Get the data and send it to the browser via the controller
|
||||
controller.enqueue(value);
|
||||
|
||||
// Decoding the Uint8Array to UTF-8
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
// Can have multiple messages in one chunk
|
||||
// So need to split them using delimiter (newline)
|
||||
// Then parse each message
|
||||
|
||||
// Splitting the chunk into messages
|
||||
const json_responses = (textDecoder.decode(value)).split('::end::');
|
||||
|
||||
// Parsing each message
|
||||
for (let jsonString of json_responses) {
|
||||
|
||||
// If empty string, skip
|
||||
if (!jsonString || jsonString === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parsing the JSON string
|
||||
let data = null;
|
||||
|
||||
try {
|
||||
data = JSON.parse(jsonString);
|
||||
// console.log("data", data);
|
||||
} catch (err) {
|
||||
console.log("JSON.parse ERROR. Decoded value: ", jsonString);
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// On verifie si la requête a échoué
|
||||
if (!data.success) {
|
||||
// On affiche l'erreur
|
||||
error(data?.message || "Une erreur est survenue");
|
||||
// On met le message en rouge
|
||||
cloneMessage.classList.add('text-red-500');
|
||||
// Ré-insert the last user message in the input
|
||||
chatInput.value = message;
|
||||
return;
|
||||
}
|
||||
|
||||
// On verifie si la requête a réussi
|
||||
if (data.success) {
|
||||
|
||||
// On verifie si on a un message
|
||||
if (!data.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On ajoute le message au chat
|
||||
const messageId = data.id;
|
||||
data.result.forEach((r) => {
|
||||
r.choices.forEach((data) => {
|
||||
if (data.message && data.message.content) {
|
||||
currentCloneMessage = addMessage(data.message.role, data.message.content, messageId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
push();
|
||||
});
|
||||
}
|
||||
|
||||
// Start the reading process
|
||||
push();
|
||||
},
|
||||
});
|
||||
})
|
||||
.then((stream) =>
|
||||
// Respond with our stream
|
||||
new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/html"
|
||||
}
|
||||
}).text(),
|
||||
)
|
||||
.then((result) => {
|
||||
// console.log("end", result);
|
||||
})
|
||||
.catch(err => {
|
||||
// On affiche l'erreur
|
||||
error(err);
|
||||
|
||||
// On met le message en rouge
|
||||
cloneMessage.classList.add('text-red-500');
|
||||
// Ré-insert the last user message in the input
|
||||
chatInput.value = message;
|
||||
// On retire "Laisse moi réfléchir..."
|
||||
loader(false);
|
||||
|
||||
// On affiche l'erreur
|
||||
error(err?.message || err || "Une erreur est survenue");
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// On définit la fonction qui gère l'envoi du formulaire
|
||||
const handleFormSubmit = (event) => {
|
||||
// On empêche le comportement par défaut
|
||||
event.preventDefault();
|
||||
// On récupère le message
|
||||
const message = chatInput.value;
|
||||
// On envoie le message
|
||||
sendMessage(message);
|
||||
// On vide le champ
|
||||
chatInput.value = '';
|
||||
};
|
||||
// On définit la fonction qui gère l'envoi du formulaire
|
||||
const handleFormSubmit = (event) => {
|
||||
// On empêche le comportement par défaut
|
||||
event.preventDefault();
|
||||
// On récupère le message
|
||||
const message = chatInput.value;
|
||||
// On envoie le message
|
||||
sendMessage(message);
|
||||
// On vide le champ
|
||||
chatInput.value = '';
|
||||
};
|
||||
|
||||
// On ajoute l'écouteur d'évènement sur le formulaire
|
||||
chatForm.addEventListener('submit', handleFormSubmit);
|
||||
});
|
||||
</script>
|
||||
// On ajoute l'écouteur d'évènement sur le formulaire
|
||||
chatForm.addEventListener('submit', handleFormSubmit);
|
||||
});
|
||||
</script>
|
||||
<?php View::end(); ?>
|
Loading…
Reference in New Issue