|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!doctype html> |
|
<html lang="en"> |
|
{{template "views/partials/head" .}} |
|
<script defer src="/static/chat.js"></script> |
|
<style> |
|
body { |
|
overflow: hidden; |
|
} |
|
</style> |
|
<body class="bg-gray-900 text-gray-200" x-data="{ key: $store.chat.key }"> |
|
<div class="flex flex-col min-h-screen"> |
|
|
|
{{template "views/partials/navbar" .}} |
|
<div class="chat-container mt-2 mr-2 ml-2 mb-2 bg-gray-800 shadow-lg rounded-lg" > |
|
|
|
<div class="border-b border-gray-700 p-4" x-data="{ component: 'menu' }"> |
|
|
|
<div class="flex items-center justify-between"> |
|
|
|
<h1 class="text-lg font-semibold"> <i class="fa-solid fa-comments"></i> Chat with {{.Model}} <a href="https://localai.io/features/text-generation/" target="_blank" > |
|
<i class="fas fa-circle-info pr-2"></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 float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong" |
|
> |
|
Clear chat 🔥 |
|
</button> |
|
<button @click="component = 'key'" title="Update API key" |
|
class="m-2 float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong" |
|
>Set API Key🔑</button> |
|
<button @click="component = 'system_prompt'" title="System Prompt" |
|
class="m-2 float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong" |
|
>Set system prompt</button> |
|
</div> |
|
<form x-show="component === 'key'" id="key"> |
|
<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"> |
|
<i class="fa-solid fa-arrow-right"></i> |
|
</button> |
|
</form> |
|
<form x-show="component === 'system_prompt'" id="system_prompt"> |
|
<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"> |
|
<i class="fa-solid fa-arrow-right"></i> |
|
</button> |
|
</form> |
|
|
|
<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 }} |
|
{{ if eq . $model }} |
|
<option value="/chat/{{.}}" selected class="bg-gray-700 text-white">{{.}}</option> |
|
{{ else }} |
|
<option value="/chat/{{.}}" class="bg-gray-700 text-white">{{.}}</option> |
|
{{ end }} |
|
{{ end }} |
|
</select> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="chat-messages p-4" id="chat" x-data="{history: $store.chat.history}"> |
|
<p id="usage" x-show="history.length === 0"> |
|
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-600" x-text="message.role === 'user' ? 'User' : 'Assistant ({{.Model}})'"></span> |
|
<template x-if="message.role === 'user'"> |
|
<div class="p-2 flex-1 rounded" :class="message.role" x-html="message.html"></div> |
|
</template> |
|
<template x-if="message.role === 'assistant'"> |
|
<div class="p-2 flex-1 rounded" :class="message.role" x-html="message.html"></div> |
|
</template> |
|
<template x-if="message.image"> |
|
<img :src="message.image" alt="Image" class="rounded-lg mt-2 h-36 w-36"> |
|
</template> |
|
</div> |
|
</div> |
|
</template> |
|
</div> |
|
</div> |
|
|
|
<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 pl-2 border rounded w-full bg-gray-600 text-white placeholder-gray-300" |
|
required |
|
@keydown.shift="shiftPressed = true" |
|
@keyup.shift="shiftPressed = false" |
|
@keydown.enter="if (!shiftPressed) { submitPrompt($event); }" |
|
style="padding-right: 4rem;" |
|
></textarea> |
|
<span x-text="fileName" id="fileName" class="absolute right-16 top-5 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 ml-2 absolute right-10 top-3 text-lg p-2"> |
|
</button> |
|
<button type=submit><i class="fa-solid fa-circle-up text-gray-300 absolute right-2 top-3 text-lg p-2"></i></button> |
|
</div> |
|
</form> |
|
</div> |
|
<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; |
|
str = this.history[N].content; |
|
this.history[N].html = DOMPurify.sanitize( |
|
marked.parse(this.history[N].content), |
|
); |
|
} else { |
|
c = "" |
|
|
|
const lines = content.split("\n"); |
|
|
|
lines.forEach((line) => { |
|
c += DOMPurify.sanitize(marked.parse(line)); |
|
}); |
|
|
|
this.history.push({ |
|
role: role, |
|
content: content, |
|
html: c, |
|
image: 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/[email protected]/build/languages/${language}.min.js`; |
|
document.head.appendChild(script); |
|
this.languages.push(language); |
|
}); |
|
}, |
|
messages() { |
|
return this.history.map((message) => { |
|
return { |
|
role: message.role, |
|
content: message.content, |
|
image: message.image, |
|
}; |
|
}); |
|
}, |
|
}); |
|
}); |
|
</script> |
|
</div> |
|
</body> |
|
</html> |
|
|