mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-31 16:05:00 +00:00

- Change chat colors - Improve layout on small windows Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
283 lines
No EOL
12 KiB
HTML
283 lines
No EOL
12 KiB
HTML
<!--
|
|
|
|
Part of this page is based on the OpenAI Chatbot example by David Härer:
|
|
https://github.com/david-haerer/chatapi
|
|
|
|
MIT License Copyright (c) 2023 David Härer
|
|
Copyright (c) 2024 Ettore Di Giacinto
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
-->
|
|
<!doctype html>
|
|
<html lang="en">
|
|
{{template "views/partials/head" .}}
|
|
<script defer src="static/chat.js"></script>
|
|
|
|
<!-- Adjust layout classes to let the chat fill the viewport and be responsive -->
|
|
<body class="bg-slate-900 text-gray-100 flex flex-col h-screen" x-data="{ key: $store.chat.key }">
|
|
{{template "views/partials/navbar" .}}
|
|
|
|
<!-- Container fills available space (flex-1) and allows scrolling if needed -->
|
|
<div class="flex flex-col flex-1 overflow-hidden">
|
|
|
|
<!-- Header can stay at the top -->
|
|
<div class="border-b border-gray-700 p-4" x-data="{ component: 'menu' }">
|
|
<div class="flex items-center justify-between flex-wrap gap-2">
|
|
<h1 class="text-lg font-semibold flex items-center">
|
|
<i class="fa-solid fa-comments mr-2"></i>
|
|
Chat
|
|
{{ if .Model }} with {{.Model}} {{ end }}
|
|
<a class="ml-2" href="browse?term={{.Model}}">
|
|
<i class="fas fa-brain pr-2"></i>
|
|
</a>
|
|
<a href="https://localai.io/features/text-generation/" target="_blank">
|
|
<i class="fas fa-circle-info"></i>
|
|
</a>
|
|
</h1>
|
|
<div x-show="component === 'menu'" id="menu">
|
|
<button
|
|
@click="$store.chat.clear()"
|
|
id="clear"
|
|
title="Clear chat history"
|
|
data-twe-ripple-init
|
|
data-twe-ripple-color="light"
|
|
class="m-2 inline-block rounded bg-primary px-4 py-1 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 focus:bg-primary-accent-300 focus:outline-none"
|
|
>
|
|
Clear chat 🔥
|
|
</button>
|
|
<button
|
|
@click="component = 'key'"
|
|
title="Update API key"
|
|
class="m-2 inline-block rounded bg-primary px-4 py-1 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 focus:bg-primary-accent-300 focus:outline-none"
|
|
>
|
|
Set API Key🔑
|
|
</button>
|
|
<button
|
|
@click="component = 'system_prompt'"
|
|
title="System Prompt"
|
|
class="m-2 inline-block rounded bg-primary px-4 py-1 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 focus:bg-primary-accent-300 focus:outline-none"
|
|
>
|
|
Set system prompt
|
|
</button>
|
|
</div>
|
|
<!-- API Key form -->
|
|
<form x-show="component === 'key'" id="key" class="flex items-center gap-2">
|
|
<input
|
|
type="password"
|
|
id="apiKey"
|
|
name="apiKey"
|
|
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none"
|
|
placeholder="OpenAI API Key"
|
|
x-model.lazy="key"
|
|
/>
|
|
<button
|
|
@click="component = 'menu'"
|
|
type="submit"
|
|
title="Save API key"
|
|
class="text-white"
|
|
>
|
|
<i class="fa-solid fa-arrow-right"></i>
|
|
</button>
|
|
</form>
|
|
<!-- System Prompt form -->
|
|
<form x-show="component === 'system_prompt'" id="system_prompt" class="flex flex-col gap-2 mt-2">
|
|
<textarea
|
|
type="text"
|
|
id="systemPrompt"
|
|
name="systemPrompt"
|
|
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none"
|
|
placeholder="System prompt"
|
|
x-model.lazy="system_prompt"
|
|
></textarea>
|
|
<button
|
|
@click="component = 'menu'"
|
|
type="submit"
|
|
title="Save Prompt"
|
|
class="self-end text-white"
|
|
>
|
|
<i class="fa-solid fa-arrow-right"></i>
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Model selection -->
|
|
<select
|
|
x-data="{ link : '' }"
|
|
x-model="link"
|
|
x-init="$watch('link', value => window.location = link)"
|
|
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none"
|
|
>
|
|
<option value="" disabled class="text-gray-400">Select a model</option>
|
|
{{ $model:=.Model}}
|
|
{{ range .ModelsConfig }}
|
|
{{ $cfg := . }}
|
|
{{ range .KnownUsecaseStrings }}
|
|
{{ if eq . "FLAG_CHAT" }}
|
|
<option
|
|
value="chat/{{$cfg.Name}}"
|
|
{{ if eq $cfg.Name $model }} selected {{end}}
|
|
class="bg-gray-700 text-white"
|
|
>
|
|
{{$cfg.Name}}
|
|
</option>
|
|
{{ end }}
|
|
{{ end }}
|
|
{{ end }}
|
|
{{ range .ModelsWithoutConfig }}
|
|
<option
|
|
value="chat/{{.}}"
|
|
{{ if eq . $model }} selected {{ end }}
|
|
class="bg-gray-700 text-white"
|
|
>
|
|
{{.}}
|
|
</option>
|
|
{{end}}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main chat area (flex-1 for expansion) -->
|
|
<div class="flex-1 p-4 overflow-auto" id="chat" x-data="{history: $store.chat.history}">
|
|
<p id="usage" x-show="history.length === 0" class="text-gray-300">
|
|
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.
|
|
For models that support images, you can upload an image by clicking the paperclip
|
|
<i class="fa-solid fa-paperclip"></i> icon.
|
|
</p>
|
|
<div id="messages">
|
|
<template x-for="message in history">
|
|
<div class="message flex items-start space-x-2 my-2">
|
|
<i
|
|
class="fa-solid h-8 w-8"
|
|
:class="message.role === 'user' ? 'fa-user' : 'fa-robot'"
|
|
></i>
|
|
<div class="flex flex-col flex-1">
|
|
<span
|
|
class="text-xs font-semibold text-gray-400"
|
|
x-text="message.role === 'user' ? 'User' : 'Assistant ({{.Model}})'"
|
|
></span>
|
|
<template x-if="message.role === 'user'">
|
|
<div class="p-2 flex-1 rounded bg-gray-700 text-white" x-html="message.html"></div>
|
|
</template>
|
|
<template x-if="message.role === 'assistant'">
|
|
<div class="p-2 flex-1 rounded bg-gray-700 text-white" x-html="message.html"></div>
|
|
</template>
|
|
<template x-if="message.image">
|
|
<img :src="message.image" alt="Image" class="rounded-lg mt-2 max-w-xs">
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chat Input -->
|
|
<div class="p-4 border-t border-gray-700" x-data="{ inputValue: '', shiftPressed: false, fileName: '' }">
|
|
<div id="loader" class="my-2 loader" style="display: none;"></div>
|
|
<input id="chat-model" type="hidden" value="{{.Model}}">
|
|
<input
|
|
id="input_image"
|
|
type="file"
|
|
style="display: none;"
|
|
@change="fileName = $event.target.files[0].name"
|
|
/>
|
|
<form id="prompt" action="chat/{{.Model}}" method="get" @submit.prevent="submitPrompt">
|
|
<div class="relative w-full">
|
|
<textarea
|
|
id="input"
|
|
name="input"
|
|
x-model="inputValue"
|
|
placeholder="Send a message..."
|
|
class="p-2 pr-16 border rounded w-full bg-gray-600 text-gray-100 placeholder-gray-300 focus:outline-none resize-none"
|
|
required
|
|
@keydown.shift="shiftPressed = true"
|
|
@keyup.shift="shiftPressed = false"
|
|
@keydown.enter="if (!shiftPressed) { submitPrompt($event); }"
|
|
rows="3"
|
|
></textarea>
|
|
<span x-text="fileName" id="fileName" class="absolute right-16 top-3 text-gray-300 text-sm mr-2"></span>
|
|
<button
|
|
type="button"
|
|
onclick="document.getElementById('input_image').click()"
|
|
class="fa-solid fa-paperclip text-gray-300 absolute right-10 top-3 text-lg p-2 hover:text-gray-100"
|
|
title="Attach an image"
|
|
></button>
|
|
<button
|
|
type="submit"
|
|
class="absolute right-2 top-3 text-lg p-2 text-gray-300 hover:text-gray-100"
|
|
title="Send message"
|
|
>
|
|
<i class="fa-solid fa-circle-up"></i>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alpine store initialization -->
|
|
<script>
|
|
document.addEventListener("alpine:init", () => {
|
|
Alpine.store("chat", {
|
|
history: [],
|
|
languages: [undefined],
|
|
clear() {
|
|
this.history.length = 0;
|
|
},
|
|
add(role, content, image) {
|
|
const N = this.history.length - 1;
|
|
if (this.history.length && this.history[N].role === role) {
|
|
this.history[N].content += content;
|
|
this.history[N].html = DOMPurify.sanitize(
|
|
marked.parse(this.history[N].content)
|
|
);
|
|
} else {
|
|
let c = "";
|
|
const lines = content.split("\n");
|
|
lines.forEach((line) => {
|
|
c += DOMPurify.sanitize(marked.parse(line));
|
|
});
|
|
this.history.push({ role, content, html: c, image });
|
|
}
|
|
const parser = new DOMParser();
|
|
const html = parser.parseFromString(
|
|
this.history[this.history.length - 1].html,
|
|
"text/html"
|
|
);
|
|
const code = html.querySelectorAll("pre code");
|
|
if (!code.length) return;
|
|
code.forEach((el) => {
|
|
const language = el.className.split("language-")[1];
|
|
if (this.languages.includes(language)) return;
|
|
const script = document.createElement("script");
|
|
script.src = `https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/languages/${language}.min.js`;
|
|
document.head.appendChild(script);
|
|
this.languages.push(language);
|
|
});
|
|
},
|
|
messages() {
|
|
return this.history.map((message) => ({
|
|
role: message.role,
|
|
content: message.content,
|
|
image: message.image,
|
|
}));
|
|
},
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |