ai.chat/chat.js

310 lines
8.7 KiB
JavaScript

import { LoginIdentity } from "./authenticate.js";
export class Chat {
constructor(chatApp, backup) {
this._chatApp = chatApp;
this._created = new Date().getTime();
this._messages = [];
this._waiting = false;
this._assistant = null;
this.model = chatApp.defaultModel || chatApp.models[0] || null;
if (backup){
if (backup.created)
this._created = backup.created;
if (backup.messages)
this._messages = backup.messages;
if (backup.assistant)
this._assistant = backup.assistant;
if (backup.model)
this.model = backup.model;
for (let n=0; n<this._messages.length; n++){
if (!this._messages[n] || !this._messages[n].role || !this._messages[n].content) {
this._messages.splice(n,1);
}
}
}
}
backup(){
return { exported: new Date().getTime(), created: this._created, model: this.model, messages: this._messages, assistant: this._assistant };
}
get title(){
let t = null;
if (this._messages[0] && this._messages[0].role === "user")
t = this._messages[0].content;
if (!t && this._messages[1] && this._messages[1].role === "user")
t = this._messages[1].content;
if (!t)
t = "";
// if (t.length > 32)
// return t.slice(0, 30) + "...";
return t;
}
get created(){ return this._created; }
get messages(){ return Array.from(this._messages); }
chat(content){
if (!content)
return;
this.pushMessage({
role: "user", content,
});
this._chatApp.saveChats();
this.chatRequest();
}
chatRequest(){
this._waiting = true;
this._chatApp
.chatRequest(this)
.then((assistantMessage)=>{
this.pushMessage(assistantMessage);
this._chatApp.saveChats();
})
.finally(()=>{
this._waiting = false;
this._chatApp.scrollLastMessage();
});
this._chatApp.scrollLastMessage();
}
pushMessage(message){
if (!message || !message.role || !message.content){
this._chatApp.log("pushMessage() failed. " + message);
} else {
this._messages.push(message);
}
this._chatApp.saveChats();
}
pushFile(filename, content){
this.pushMessage({
role: "file",
name: filename,
content: content,
})
}
get empty(){
return this._messages.length === (this.assistant ? 1 : 0);
}
get chatting(){
let lastMessage = this._messages.at(-1);
return (lastMessage && (lastMessage.role === "user"));
}
get waiting(){ return this._waiting; }
rebuildChat(){
if (!this._waiting){
let lastMessage = this._messages.at(-1);
while (lastMessage && lastMessage.role === "assistant"){
this._messages.pop();
lastMessage = this._messages.at(-1);
}
this.chatRequest();
}
}
get tools(){
return undefined;
}
get assistant(){ return this._assistant; }
set assistant(assistant){
this._assistant = assistant;
if (this._assistant) {
let systemPrompt = {role: "system", content: assistant.prompt};
if (this._messages.length) {
if (this._messages[0].role !== "system") {
this._messages.splice(0, 0, systemPrompt);
} else {
this._messages[0].content = systemPrompt.content;
}
} else {
this._messages.push(systemPrompt);
}
}
this._chatApp.saveChats();
}
}
export class ChatApp {
constructor(opts) {
window.$chatApp = this;
this.opts = opts;
this.chats = [];
this.baseUri = location.origin;
this.sysMessages = [];
this.loginIdentity = new LoginIdentity(this.baseUri);
this._models = null;
this.defaultModel = null;
this.restoreChats();
if (!this.chats.length) {
this.chats.push(new Chat(this));
}
this.activeChat = this.chats.at(-1);
this.sortChats();
}
get models(){
if (this._models === null){
this._models = [];
fetch(this.baseUri + "/v1/models")
.then((response) => response.json())
.then((models) => {
console.log(models);
this._models = [];
models.data.forEach((model)=>{
this._models.push(model.id || model.name);
});
if (this.defaultModel === null)
this.defaultModel = this._models[0] || null;
});
}
return Array.from(this._models);
}
log(message) {
this.sysMessages.push(message);
}
get identity() {
return this.loginIdentity;
}
restoreChats() {
let backup = JSON.parse(localStorage.getItem("ai.chats"));
if (backup && backup.chats) {
this.defaultModel = backup.defaultModel;
backup.chats.forEach((v) => {
this.chats.push(new Chat(this, v));
});
}
}
saveChats() {
localStorage.setItem("ai.chats", JSON.stringify(this.backup()));
}
backup(){
let backup = {chats: [], exported: new Date().getTime(), defaultModel: this.defaultModel};
this.chats.forEach((v) => {
backup.chats.push(v.backup());
});
return backup;
}
chat(content) {
if (this.activeChat) {
this.activeChat.chat(content);
}
}
get currentChat() {
return this.activeChat;
}
set currentChat(currentChat) {
if (this.chats.includes(currentChat)) {
this.activeChat = currentChat;
}
}
chatRequest(chat) {
return this.identity.fetch(this.baseUri + "/v1/chat/completions", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify({
model: chat.model,
messages: chat.messages,
})
}).then((response) => {
return this.fetchWithRetry(response.url + "?wait=true", 15);
})
.then(
(response) => {
if (response.ok) {
response.json()
.then((json) => {
let assistantMessage = json.choices[0].message;
if (!assistantMessage) {
this.log(`chatRequest failed with: ${JSON.stringify(json)}`);
} else {
chat.pushMessage(assistantMessage);
}
});
}
this.log(`chatRequest (I) failed with: ${response}`);
},
(reason) => {
this.log(`chatRequest (II) failed with: ${reason}`);
}).catch(error => {
this.log(`chatRequest (III) failed with: ${error}`);
console.error(error);
});
}
newChat(jsonImport) {
this.chats.push(new Chat(this, jsonImport));
this.activeChat = this.chats.at(-1);
this.sortChats();
}
sortChats(){
this.chats.sort((a,b)=>a.created - b.created);
}
scrollLastMessage() {
setTimeout(() => {
document.querySelector("#chatMessages > div:last-of-type").scrollIntoView({behavior: "smooth"});
}, 200);
}
removeChat(chat) {
let idxChat = this.chats.indexOf(chat);
if (idxChat >= 0)
this.chats.splice(idxChat, 1);
this.saveChats();
}
async fetchWithRetry(url, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
const response = await this.identity.fetch(url);
if (response.status !== 204) {
return response;
}
retries++;
console.log(`No content received (Status 204). Retrying... (${retries}/${maxRetries})`);
// Warte für 1 Sekunde, bevor du es erneut versuchst.
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error(`Failed to get content after ${maxRetries} retries.`);
}
}