Initial release

This commit is contained in:
Julian Müller (ChaoticByte) 2023-04-30 12:16:48 +02:00
parent 9e3f7b8a93
commit cd6036eff4
11 changed files with 670 additions and 2 deletions

0
frontend/__init__.py Normal file
View file

42
frontend/app.py Normal file
View file

@ -0,0 +1,42 @@
# Copyright (c) 2023 Julian Müller (ChaoticByte)
from pathlib import Path
from sanic import Sanic
from sanic import Request
from sanic import json
from sanic import redirect
# App
app = Sanic("Eucalyptus")
# Static files
static_dir = Path(__file__).parent / "static"
app.static("/ui", static_dir, index="index.html")
# Routes
@app.get("/")
async def redirect_to_ui(request: Request):
return redirect("/ui/")
@app.get("/config")
async def frontend_config(request: Request):
return json(app.config.frontend_config)
# Security Headers
@app.on_response
async def security_headers(_, response):
response.headers.update({
"X-Frame-Options": "deny",
"X-Content-Type-Options": "nosniff",
"Content-Security-Policy": f"default-src 'self'; style-src 'unsafe-inline' 'self'; connect-src 'self' {app.config.frontend_config['api_url']}",
"X-Permitted-Cross-Domain-Policies": "none",
"Referrer-Policy": "no-referrer",
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "same-origin"
})

View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Copyright (c) 2023 Julian Müller (ChaoticByte) -->
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/ui/style.css">
<title>Eucalyptus</title>
</head>
<body class="flex">
<div class="main flex flex-column">
<div id="messages" class="messages flex flex-column"></div>
<div class="input-container flex">
<textarea id="text-input" class="text-input" rows="1" placeholder="Press Ctrl+Enter to send" autofocus></textarea>
<button id="send-btn" class="send-btn icon-button">
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="M120 896V651l302-75-302-77V256l760 320-760 320Z"/></svg>
</button>
</div>
</div>
<div class="sidepanel flex flex-column">
<div class="max-width">Settings</div>
<div class="settings flex flex-column">
<div class="setting flex">
<div>max_tokens</div>
<div><input type="number" id="settings-max-tokens" min="16" value="100"></div>
</div>
<div class="setting flex">
<div>temperature</div>
<div><input type="number" id="settings-temperature" min="0.0" max="2.0" step="0.01" value="0.8"></div>
</div>
<div class="setting flex">
<div>top_p</div>
<div><input type="number" id="settings-top-p" min="0.0" max="1.0" step="0.01" value="0.95"></div>
</div>
</div>
<div class="flex">
<button id="reset-history-btn" class="icon-button">
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m361 757 119-121 120 121 47-48-119-121 119-121-47-48-120 121-119-121-48 48 120 121-120 121 48 48ZM261 936q-24 0-42-18t-18-42V306h-41v-60h188v-30h264v30h188v60h-41v570q0 24-18 42t-42 18H261Z"/></svg>
</button>
</div>
</div>
<script src="/ui/main.js"></script>
</body>
</html>

159
frontend/static/main.js Normal file
View file

@ -0,0 +1,159 @@
// Copyright (c) 2023 Julian Müller (ChaoticByte)
(() => {
// Koala specific keywords
const conversationBeginning = "BEGINNING OF CONVERSATION:";
const userKeyword = " USER: ";
const assistantKeyword = " GPT:";
const koalaStopSequence = "</s>";
// Get frontend config
let frontend_config = null;
fetch("/config")
.then(r => {
return r.json();
})
.then(j => {
frontend_config = j;
});
// Message Context
let conversation = [conversationBeginning];
// Elements - Sidebar
const settingsMaxTokensElement = document.getElementById("settings-max-tokens");
const settingsTemperatureElement = document.getElementById("settings-temperature");
const settingsTopPElement = document.getElementById("settings-top-p");
const resetHistoryButtonElement = document.getElementById("reset-history-btn");
// Elements - Main
const messageHistoryContainer = document.getElementById("messages");
const textInputElement = document.getElementById("text-input");
const sendButtonElement = document.getElementById("send-btn");
// API requests
async function apiCompletion(prompt, settings) {
const bodyData = JSON.stringify({
"prompt": prompt,
"stop": [koalaStopSequence],
"max_tokens": settings.max_tokens,
"temperature": settings.temperature,
"top_p": settings.top_p
});
const response = await fetch(frontend_config.api_url + "/v1/completions", {
method: "post",
cache: "no-cache",
body: bodyData,
headers: {
"content-type": "application/json"
}
});
const responseData = await response.json();
return responseData["choices"][0]["text"];
}
// User-defined settings
function getSettings() {
return {
max_tokens: settingsMaxTokensElement.value,
temperature: settingsTemperatureElement.value,
top_p: settingsTopPElement.value
}
}
// Chat
const MessageType = {
USER: {
name: "User",
class: "message-bg-user"
},
ASSISTANT: {
name: "Koala",
class: "message-bg-assistant"
}
}
function addMessage(message, type) {
if (type == MessageType.USER) {
conversation.push(userKeyword + message + assistantKeyword);
}
else { conversation.push(message); }
// UI
let messageTypeElem = document.createElement("div");
messageTypeElem.classList.add("message-type");
messageTypeElem.innerText = type.name;
let messageTextElem = document.createElement("div");
messageTextElem.classList.add("message-text");
messageTextElem.innerText = message;
let messageElem = document.createElement("div");
messageElem.classList.add("message");
messageElem.classList.add(type.class);
messageElem.appendChild(messageTypeElem);
messageElem.appendChild(messageTextElem);
messageHistoryContainer.appendChild(messageElem);
}
function disableInput() {
sendButtonElement.disabled = true;
textInputElement.disabled = true;
}
function enableInput() {
sendButtonElement.disabled = false;
textInputElement.disabled = false;
// focus text input
textInputElement.focus();
}
async function chat() {
if (frontend_config == null) {
console.log("Couldn't fetch frontend configuration.");
}
else {
disableInput();
let input = textInputElement.value.trim();
if (input == "") {
enableInput();
}
else {
textInputElement.value = "";
addMessage(input, MessageType.USER);
let prompt = conversation.join("");
let settings = getSettings();
apiCompletion(prompt, settings).then(r => {
addMessage(r, MessageType.ASSISTANT);
enableInput();
});
}
}
}
function resetHistory() {
conversation = [conversationBeginning];
messageHistoryContainer.innerText = "";
}
// Event Listeners
resetHistoryButtonElement.addEventListener("click", resetHistory);
sendButtonElement.addEventListener("click", chat);
textInputElement.addEventListener("keypress", e => {
// Send via Ctrl+Enter
if (e.key == "Enter" && e.ctrlKey) {
chat();
}
});
textInputElement.addEventListener("input", e => {
// Calculate Line height
textInputElement.style.removeProperty("height");
let newHeight = textInputElement.scrollHeight;
textInputElement.style.height = newHeight.toString() + "px";
});
})();

164
frontend/static/style.css Normal file
View file

@ -0,0 +1,164 @@
/* Copyright (c) 2023 Julian Müller (ChaoticByte) */
:root {
--background: #1f1f1f;
--background2: #303030;
--background3: #161616;
--background4: #131313;
--button-bg: #3b3b3b;
--button-bg2: #4f4f4f;
--icon-button-fill: #ffffff;
--send-icon-button-fill: #29c76d;
--color: #fafafa;
--padding: .5rem;
--border-radius: .5rem;
}
body {
margin: 0;
padding: 0;
background: var(--background);
color: var(--color);
font-family: sans-serif;
flex-direction: row;
min-height: 100vh;
}
input[type="number"] {
width: 4rem;
}
.sidepanel {
gap: var(--padding);
align-items: flex-end;
padding: 1rem;
padding-left: 0;
min-width: fit-content;
}
.settings {
margin-top: 1rem;
margin-bottom: 1rem;
gap: .5rem;
}
.setting {
gap: .5rem;
}
.setting > div:first-child {
flex-grow: 1;
}
.main {
flex-grow: 1;
justify-content: flex-end;
padding: 1rem;
}
.messages {
gap: 1.1rem;
padding-bottom: var(--padding);
overflow-y: scroll;
max-height: 89vh;
}
.message {
display: flex;
flex-direction: row;
gap: var(--padding);
padding: var(--padding);
border-radius: var(--border-radius);
max-width: fit-content;
}
button {
padding: .5rem .7rem;
border: none;
outline: none;
color: var(--color);
background: var(--button-bg);
border-radius: var(--border-radius);
font-size: .9rem;
}
button:disabled, input:disabled, textarea:disabled {
opacity: 50%;
}
button:hover {
background: var(--button-bg2);
}
.flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.max-width {
width: 100%;
}
.message-bg-assistant {
background: var(--background2);
}
.message-bg-user {
background: var(--background3);
}
.message-type {
min-width: 3.5rem;
padding-left: .1rem;
}
.input-container {
margin-top: auto;
flex-direction: row;
align-items: center;
gap: .5rem;
}
.text-input {
margin: 0;
flex-grow: 1;
background-color: var(--background4);
border: none;
outline: none;
padding: .8rem 1.1rem;
border-radius: var(--border-radius);
color: var(--color);
resize: none;
font-size: .9rem;
}
.icon-button {
padding: .2rem;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
}
.icon-button:hover {
background: transparent;
}
.icon-button > svg {
height: 1.8rem;
width: auto;
fill: var(--icon-button-fill);
}
.icon-button:hover > svg {
transform: scale(1.1);
}
.send-btn > svg {
height: 2.2rem;
fill: var(--send-icon-button-fill);
}