310 lines
8.7 KiB
JavaScript
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.`);
|
|
}
|
|
|
|
} |