296 lines
14 KiB
HTML
296 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="stylesheet" href="contrib/lnstyles/css/lnstyles.css">
|
|
<link rel="stylesheet" href="contrib/highlight/default.min.css">
|
|
<link rel="stylesheet" href="chat.css">
|
|
<link rel="stylesheet" href="code.css">
|
|
<link rel="icon" type="image/svg+xml" href="/contrib/lnstyles/svg/ln.svg">
|
|
|
|
<script type="application/javascript" src="./contrib/highlight/highlight.min.js"></script>
|
|
|
|
<title>ai.chat - local ai test chat</title>
|
|
</head>
|
|
<body class="bg-white p-2 fill">
|
|
<div id="app" class="display-flex flex-row" v-cloak>
|
|
<div v-if="!chatApp.identity.authenticated" class="flex-grow-1">
|
|
<div class="row p-4">
|
|
<div class="sz-12 vh-20"></div>
|
|
<div class="sz-12">
|
|
<div class="m-auto" style="width: min-content;">
|
|
<div class="text-center mb-2 border-bottom">
|
|
<h1>AI.chat</h1>
|
|
</div>
|
|
<div class="">Benutzername</div>
|
|
<div class="p-1 text-center"><input type="text" v-model="loginUsername"></div>
|
|
<div class="">Passwort</div>
|
|
<div class="p-1 text-center"><input type="password" v-model="loginPassword"></div>
|
|
<div class="pt-2 text-center">
|
|
<button @click="authenticate">Anmelden...</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template v-if="chatApp.identity.authenticated">
|
|
<div class="flex-grow-0 display-flex flex-column" style="min-width: 400px;">
|
|
<h1 class="pl-1 flex-grow-0">AI.chat</h1>
|
|
<div class="flex-grow-0">
|
|
<v-svg
|
|
class="large hover"
|
|
tooltip="neu..."
|
|
src="contrib/lnstyles/svg/neutral/newdocument.svg"
|
|
@click="newChat();"></v-svg>
|
|
<v-svg
|
|
class="large hover"
|
|
tooltip="import..."
|
|
src="contrib/lnstyles/svg/neutral/import.svg"
|
|
@click="importChats();"></v-svg>
|
|
<v-svg
|
|
class="large hover"
|
|
tooltip="export..."
|
|
src="contrib/lnstyles/svg/neutral/export.svg"
|
|
@click="exportChats();"></v-svg>
|
|
|
|
<select v-model="chatApp.defaultModel"
|
|
@change="chatApp.saveChats();">
|
|
<option v-for="model in chatApp.models"
|
|
:value="model">{{ model }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex-grow-1 overflow-scroll">
|
|
<div v-for="chat in chatApp.chats.slice().reverse()"
|
|
class="border-radius-1 border-1 row m-1 p-1"
|
|
:class="{ 'bg-heading': (chatApp.currentChat == chat), 'bg-lightblue': (chat.waiting) }"
|
|
@click="setCurrentChat(chat);"
|
|
>
|
|
<div class="sz-11 row">
|
|
<small>{{ new Date(chat.created).toLocaleString() }}</small>
|
|
<small><i>{{ chat.model }}</i></small>
|
|
<small v-if="chat.assistant"
|
|
class="flex-grow-1 text-right">{{ chat.assistant.name }}</small>
|
|
</div>
|
|
<div class="sz-1">
|
|
<img src="contrib/lnstyles/svg/triangle-down.svg"
|
|
class="position-absolute" style="width: 1em; right: 2em; top: 1em;"
|
|
@click="exportChat(chat);">
|
|
<img src="contrib/lnstyles/svg/delete.svg"
|
|
class="position-absolute" style="width: 1em; right: 1em; top: 1em;"
|
|
@click="chatApp.removeChat(chat);">
|
|
</div>
|
|
<div class="sz-12 text-overflow-ellipsis smaller">{{ chat.title }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 display-flex flex-column">
|
|
<div class="sz-9 bg-white border-radius-2 display-flex flex-column h-100 position-relative">
|
|
<div class="bg-white border-radius-2 flex-grow-1 overflow-scroll smaller px-2 pt-2"
|
|
style="max-height: 100%; padding-bottom: 10em;">
|
|
<div id="chatMessages">
|
|
<template v-for="chatMessage in chatApp.currentChat.messages">
|
|
<div
|
|
v-if="chatMessage.role !== 'system'"
|
|
class="border-radius-2 p-2 mb-2 position-relative"
|
|
:class="{
|
|
'bg-lightgreen': (chatMessage.role === 'user'),
|
|
'ml-4': (chatMessage.role === 'user') || (chatMessage.role === 'file'),
|
|
'bg-lightgray': (chatMessage.role === 'file'),
|
|
'bg-assistant': (chatMessage.role === 'assistant'),
|
|
'mr-4': (chatMessage.role === 'assistant')
|
|
}"
|
|
>
|
|
<v-svg
|
|
class="position-absolute hover" style="right: 0.5em; top: 0.5em;"
|
|
src="contrib/lnstyles/svg/neutral/copy.svg"
|
|
@click="copyToClipboard($event);"
|
|
></v-svg>
|
|
<v-svg
|
|
v-if="chatMessage.role === 'file'"
|
|
class="position-absolute hover" style="right: 2.5em; top: 0.5em;"
|
|
:src="chatMessage.expanded ? 'contrib/lnstyles/svg/neutral/dropdown-close.svg' : 'contrib/lnstyles/svg/neutral/dropdown-open.svg'"
|
|
@click="chatMessage.expanded = !chatMessage.expanded"
|
|
></v-svg>
|
|
|
|
<v-message v-if="chatMessage.role !== 'file'"
|
|
:format="convertMarkdown"
|
|
:content="chatMessage.content"
|
|
></v-message>
|
|
<div v-if="chatMessage.role === 'file'">
|
|
<div>Datei <b>{{ chatMessage.name }}</b> ({{ chatMessage.content.length }} Bytes)
|
|
</div>
|
|
<div v-if="chatMessage.expanded"
|
|
class="pre">
|
|
<code class="hljs" v-html="highlightSyntax(chatMessage.content)"></code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="p-1 position-absolute m-1 border border-radius-2"
|
|
style="left: 0; right: 0; bottom: 0; z-index: 100; background-color: rgba(255,255,255,0.9);">
|
|
<div class="row">
|
|
<div class="sz-12 small">
|
|
<select v-model="chatApp.currentChat.model">
|
|
<option v-for="model in chatApp.models"
|
|
:value="model">{{ model }}</option>
|
|
</select>
|
|
<select v-model="chatApp.currentChat.assistant">
|
|
<option :value="null">-</option>
|
|
<option v-for="assistant in assistants"
|
|
:value="assistant"
|
|
>{{ assistant.name }}
|
|
</option>
|
|
</select>
|
|
<button @click="attachFile();">Dateien einfügen...</button>
|
|
<span v-if="chatApp.currentChat.waiting">warte auf Antwort...</span>
|
|
<button v-if="chatApp.currentChat.chatting && !chatApp.currentChat.waiting"
|
|
@click="chatApp.currentChat.rebuildChat();">nochmal versuchen...
|
|
</button>
|
|
<button v-if="!chatApp.currentChat.chatting && !chatApp.currentChat.waiting && !chatApp.currentChat.empty"
|
|
@click="chatApp.currentChat.rebuildChat();">wiederholen...
|
|
</button>
|
|
</div>
|
|
<div :contenteditable="!chatApp.currentChat.chatting || undefined"
|
|
class="sz-12 border-radius-1 p-1 smaller overflow-auto"
|
|
id="prompt"
|
|
style="min-height: 3em; max-height: 8em; border: 1px solid lightgray;"
|
|
@input="promptChanged" @keypress="promptKeypress"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
</div>
|
|
<script type="application/javascript" src="markdown-it.min.js"></script>
|
|
<script type="module">
|
|
import {createApp, reactive, nextTick} from "./contrib/lnstyles/js/vue.esm-browser.js";
|
|
import {ChatApp} from "./chat.js";
|
|
import {LNS} from "./contrib/lnstyles/js/lnstyles.js";
|
|
import {vsvg} from "./vsvg.js";
|
|
import {vmessage} from "./v-message.js";
|
|
|
|
var $ = LNS;
|
|
|
|
let config = await fetch("config.json").then((r) => r.json());
|
|
let markdown = markdownit();
|
|
markdown.use((md) => {
|
|
const temp = md.renderer.rules.fence.bind(md.renderer.rules)
|
|
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
|
const token = tokens[idx]
|
|
const code = token.content.trim()
|
|
if (token.info.length > 0) {
|
|
return `<pre><code class="hljs">${hljs.highlightAuto(code, [token.info]).value}</code></pre>`
|
|
}
|
|
return temp(tokens, idx, options, env, slf)
|
|
}
|
|
});
|
|
|
|
var defaultRender = markdown.renderer.rules.link_open || function (tokens, idx, options, env, self) {
|
|
return self.renderToken(tokens, idx, options);
|
|
};
|
|
markdown.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
|
// Add a new `target` attribute, or replace the value of the existing one.
|
|
tokens[idx].attrSet('target', '_blank');
|
|
|
|
// Pass the token to the default renderer.
|
|
return defaultRender(tokens, idx, options, env, self);
|
|
};
|
|
|
|
var app = createApp({
|
|
data() {
|
|
let chatApp = new ChatApp();
|
|
reactive(chatApp).loginIdentity.restoreToken();
|
|
return {
|
|
chatApp: chatApp,
|
|
currentMessageText: "",
|
|
assistants: config.assistants,
|
|
loginUsername: "",
|
|
loginPassword: "",
|
|
$,
|
|
};
|
|
},
|
|
methods: {
|
|
promptKeypress(evt) {
|
|
if ((evt.which === 13) && !evt.shiftKey) {
|
|
evt.preventDefault();
|
|
this.chatApp.chat(evt.target.innerText);
|
|
evt.target.innerText = "";
|
|
}
|
|
},
|
|
promptChanged(evt){
|
|
this.currentMessageText = evt.target.innerText;
|
|
},
|
|
convertMarkdown(md) {
|
|
let mdHtml = markdown.render(md);
|
|
return mdHtml;
|
|
},
|
|
highlightSyntax(src) {
|
|
return hljs.highlightAuto(src).value;
|
|
},
|
|
newChat() {
|
|
this.chatApp.newChat();
|
|
},
|
|
copyToClipboard(evt) {
|
|
let e = evt.target.parentElement.parentElement;
|
|
|
|
navigator.clipboard.writeText(e.innerText)
|
|
.then(
|
|
() => {
|
|
console.log("copy succeded");
|
|
},
|
|
() => {
|
|
console.log("copy failed");
|
|
}
|
|
)
|
|
},
|
|
authenticate() {
|
|
console.log("authenticate", this.loginUsername);
|
|
this.chatApp.identity.authenticate(this.loginUsername, this.loginPassword);
|
|
},
|
|
exportChat(chat) {
|
|
$.saveAs(chat.title + '.json', chat.backup());
|
|
},
|
|
exportChats() {
|
|
$.saveAs('ai.chats.json', this.chatApp.backup());
|
|
},
|
|
importChats() {
|
|
$.openFile((filename, content) => {
|
|
let jsonImport = JSON.parse(content);
|
|
if (!jsonImport.exported) {
|
|
alert("Datei ist kein ai.chat export");
|
|
} else if (jsonImport.chats) {
|
|
jsonImport.chats.forEach((jsonChat) => {
|
|
this.chatApp.newChat(jsonChat);
|
|
});
|
|
} else if (jsonImport.messages) {
|
|
this.chatApp.newChat(jsonImport);
|
|
}
|
|
}, [".json"])
|
|
},
|
|
attachFile() {
|
|
$.openFile((files) => {
|
|
files.forEach((file) => {
|
|
this.chatApp.currentChat.pushFile(file.filename, file.content);
|
|
});
|
|
}, {multiple: true,});
|
|
},
|
|
setCurrentChat(chat) {
|
|
this.chatApp.currentChat = chat;
|
|
nextTick(() => {
|
|
document.querySelector("#chatMessages > div:last-of-type").scrollIntoView({behavior: "instant"});
|
|
})
|
|
},
|
|
}
|
|
})
|
|
.component("v-svg", vsvg)
|
|
.component("v-message", vmessage)
|
|
.mount("#app");
|
|
|
|
</script>
|
|
</body>
|
|
</html> |