Now using Streamed request/response for gpt chat + gixes ad opti

This commit is contained in:
Thommy Bucaille 2023-12-30 08:03:11 +01:00
parent 2eb327d3b0
commit 75e07eedcb
4 changed files with 458 additions and 211 deletions

View File

@ -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 à Equemauville dans le Calvados. Angélique et Bastien sont à 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)[

View File

@ -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

View File

@ -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;

View File

@ -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(); ?>