cleanup styling, show reasoning, fixes & improvements
3
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.env
|
||||
.env
|
||||
debug.json
|
24
chat.go
@@ -6,16 +6,18 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/revrost/go-openrouter"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Model string `json:"model"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Messages []Message `json:"messages"`
|
||||
@@ -24,7 +26,8 @@ type Request struct {
|
||||
func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
|
||||
var request openrouter.ChatCompletionRequest
|
||||
|
||||
if _, ok := ModelMap[r.Model]; !ok {
|
||||
model, ok := ModelMap[r.Model]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown model: %q", r.Model)
|
||||
}
|
||||
|
||||
@@ -36,6 +39,15 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
|
||||
|
||||
request.Temperature = float32(r.Temperature)
|
||||
|
||||
prompt, err := BuildPrompt(r.Prompt, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if prompt != "" {
|
||||
request.Messages = append(request.Messages, openrouter.SystemMessage(prompt))
|
||||
}
|
||||
|
||||
for index, message := range r.Messages {
|
||||
if message.Role != openrouter.ChatMessageRoleSystem && message.Role != openrouter.ChatMessageRoleAssistant && message.Role != openrouter.ChatMessageRoleUser {
|
||||
return nil, fmt.Errorf("[%d] invalid role: %q", index+1, message.Role)
|
||||
@@ -44,7 +56,7 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
|
||||
request.Messages = append(request.Messages, openrouter.ChatCompletionMessage{
|
||||
Role: message.Role,
|
||||
Content: openrouter.Content{
|
||||
Text: message.Content,
|
||||
Text: message.Text,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -74,6 +86,10 @@ func HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
request.Stream = true
|
||||
|
||||
// DEBUG
|
||||
b, _ := json.MarshalIndent(request, "", "\t")
|
||||
os.WriteFile("debug.json", b, 0755)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
stream, err := OpenRouterStartStream(ctx, *request)
|
||||
|
51
prompts.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PromptData struct {
|
||||
Name string
|
||||
Slug string
|
||||
Date string
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed prompts/normal.txt
|
||||
PromptNormal string
|
||||
|
||||
PromptNormalTmpl = template.Must(template.New("normal").Parse(PromptNormal))
|
||||
)
|
||||
|
||||
func BuildPrompt(name string, model *Model) (string, error) {
|
||||
if name == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var tmpl *template.Template
|
||||
|
||||
switch name {
|
||||
case "normal":
|
||||
tmpl = PromptNormalTmpl
|
||||
default:
|
||||
return "", fmt.Errorf("unknown prompt: %q", name)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := tmpl.Execute(&buf, PromptData{
|
||||
Name: model.Name,
|
||||
Slug: model.ID,
|
||||
Date: time.Now().Format(time.RFC1123),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
16
prompts/normal.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
You are {{.Name}}, an AI assistant created to be helpful, harmless, and honest. Today's date is {{.Date}}.
|
||||
|
||||
Your responses should be accurate and informative. When answering questions, be direct and helpful without unnecessary preambles or acknowledgments of instructions.
|
||||
|
||||
Use markdown formatting when it improves clarity:
|
||||
- **Bold** for emphasis
|
||||
- *Italics* for softer emphasis
|
||||
- `Code` for inline code
|
||||
- Triple backticks for code blocks with language identifiers
|
||||
- Tables using pipe syntax when presenting structured data
|
||||
|
||||
Be conversational and adapt to the user's tone. Ask clarifying questions when requests are ambiguous. If you cannot answer something due to knowledge limitations, say so directly.
|
||||
|
||||
When asked about your identity, you are {{.Name}}. You don't have access to real-time information or the ability to browse the internet, so for current events or time-sensitive information, acknowledge this limitation.
|
||||
|
||||
Focus on being genuinely helpful while maintaining natural conversation flow.
|
137
static/chat.css
@@ -23,7 +23,6 @@ body {
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
background: #1e2030;
|
||||
box-shadow: 0px 0px 4px 4px #1e2030;
|
||||
margin: auto;
|
||||
margin-top: 30px;
|
||||
width: 100%;
|
||||
@@ -57,7 +56,6 @@ body {
|
||||
#message,
|
||||
.message {
|
||||
border: none;
|
||||
box-shadow: 0px 0px 4px 4px #24273a;
|
||||
border-radius: 6px;
|
||||
background: #24273a;
|
||||
font: inherit;
|
||||
@@ -69,6 +67,7 @@ body {
|
||||
max-width: 700px;
|
||||
min-width: 200px;
|
||||
width: max-content;
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
@@ -83,8 +82,8 @@ body {
|
||||
position: absolute;
|
||||
line-height: 12px;
|
||||
font-size: 12px;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
@@ -109,12 +108,11 @@ body {
|
||||
background-image: url(icons/assistant.svg);
|
||||
}
|
||||
|
||||
.message .reasoning,
|
||||
.message .text {
|
||||
display: block;
|
||||
background: transparent;
|
||||
padding: 10px 12px;
|
||||
padding-top: 28px;
|
||||
white-space: pre-line;
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -123,6 +121,62 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message:not(.expanded) .reasoning-text {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message.expanded .reasoning-text {
|
||||
padding: 10px 12px;
|
||||
background: #1e2030;
|
||||
margin-top: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.message:not(.has-reasoning) .reasoning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reasoning .toggle {
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.reasoning .toggle::after,
|
||||
.reasoning .toggle::before {
|
||||
content: "";
|
||||
background-image: url(icons/reasoning.svg);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.reasoning .toggle::after {
|
||||
background-image: url(icons/chevron.svg);
|
||||
left: unset;
|
||||
right: -2px;
|
||||
top: 1px;
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
.message.expanded .reasoning .toggle::after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 0;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.markdown p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
resize: none;
|
||||
@@ -169,46 +223,47 @@ textarea {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message.reasoning .reasoning button.toggle::before {
|
||||
animation: rotating 2s linear infinite;
|
||||
background-image: url(icons/loading.svg);
|
||||
}
|
||||
|
||||
.message .text::before {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.message:empty.receiving .text::before,
|
||||
.message.waiting .text::before {
|
||||
content: "waiting...";
|
||||
content: ". . .";
|
||||
}
|
||||
|
||||
.message.reasoning .text::before {
|
||||
content: "reasoning...";
|
||||
}
|
||||
|
||||
.message:empty.receiving .text::before {
|
||||
content: "receiving...";
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
border: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
background: #363a4f;
|
||||
}
|
||||
|
||||
#chat button {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
border: none;
|
||||
background: #363a4f;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#chat .options {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 12px;
|
||||
left: 20px;
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -230,11 +285,14 @@ select {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.reasoning .toggle::before,
|
||||
.reasoning .toggle::after,
|
||||
#bottom,
|
||||
.message .role::before,
|
||||
#clear,
|
||||
#add,
|
||||
#send,
|
||||
.message .copy,
|
||||
.message .edit,
|
||||
.message .delete,
|
||||
#chat .option label {
|
||||
@@ -246,6 +304,14 @@ select {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.message .copy {
|
||||
background-image: url(icons/copy.svg);
|
||||
}
|
||||
|
||||
.message .copy.copied {
|
||||
background-image: url(icons/check.svg);
|
||||
}
|
||||
|
||||
.message .edit {
|
||||
background-image: url(icons/edit.svg);
|
||||
}
|
||||
@@ -265,7 +331,6 @@ select {
|
||||
#model {
|
||||
width: 180px;
|
||||
padding: 2px 4px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#temperature {
|
||||
@@ -283,6 +348,10 @@ label[for="model"] {
|
||||
background-image: url(icons/model.svg);
|
||||
}
|
||||
|
||||
label[for="prompt"] {
|
||||
background-image: url(icons/prompt.svg);
|
||||
}
|
||||
|
||||
label[for="temperature"] {
|
||||
background-image: url(icons/temperature.svg);
|
||||
}
|
||||
@@ -300,7 +369,7 @@ label[for="temperature"] {
|
||||
#add,
|
||||
#send {
|
||||
bottom: 4px;
|
||||
right: 12px;
|
||||
right: 20px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-image: url(icons/send.svg);
|
||||
@@ -308,7 +377,7 @@ label[for="temperature"] {
|
||||
|
||||
#add {
|
||||
bottom: 4px;
|
||||
right: 44px;
|
||||
right: 52px;
|
||||
background-image: url(icons/add.svg);
|
||||
}
|
||||
|
||||
@@ -323,4 +392,14 @@ label[for="temperature"] {
|
||||
|
||||
.completing #send {
|
||||
background-image: url(icons/stop.svg);
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
477
static/chat.js
@@ -5,11 +5,286 @@
|
||||
$bottom = document.getElementById("bottom"),
|
||||
$role = document.getElementById("role"),
|
||||
$model = document.getElementById("model"),
|
||||
$prompt = document.getElementById("prompt"),
|
||||
$temperature = document.getElementById("temperature"),
|
||||
$add = document.getElementById("add"),
|
||||
$send = document.getElementById("send"),
|
||||
$clear = document.getElementById("clear");
|
||||
|
||||
const messages = [];
|
||||
|
||||
function uid() {
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function make(tag, ...classes) {
|
||||
const el = document.createElement(tag);
|
||||
|
||||
el.classList.add(...classes);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function render(markdown) {
|
||||
return marked.parse(DOMPurify.sanitize(markdown), {
|
||||
gfm: true,
|
||||
});
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
$messages.scroll({
|
||||
top: $messages.scrollHeight + 200,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
|
||||
class Message {
|
||||
#id;
|
||||
#role;
|
||||
#reasoning;
|
||||
#text;
|
||||
|
||||
#editing = false;
|
||||
#expanded = false;
|
||||
#state = false;
|
||||
|
||||
#_message;
|
||||
#_role;
|
||||
#_reasoning;
|
||||
#_text;
|
||||
#_edit;
|
||||
|
||||
constructor(role, reasoning, text) {
|
||||
this.#id = uid();
|
||||
this.#role = role;
|
||||
this.#reasoning = reasoning || "";
|
||||
this.#text = text || "";
|
||||
|
||||
this.#build();
|
||||
this.#render();
|
||||
|
||||
messages.push(this);
|
||||
}
|
||||
|
||||
#build() {
|
||||
// main message div
|
||||
this.#_message = make("div", "message", this.#role);
|
||||
|
||||
// message role
|
||||
this.#_role = make("div", "role", this.#role);
|
||||
|
||||
this.#_message.appendChild(this.#_role);
|
||||
|
||||
// message reasoning (wrapper)
|
||||
const _reasoning = make("div", "reasoning", "markdown");
|
||||
|
||||
this.#_message.appendChild(_reasoning);
|
||||
|
||||
// message reasoning (toggle)
|
||||
const _toggle = make("button", "toggle");
|
||||
|
||||
_toggle.textContent = "Reasoning";
|
||||
|
||||
_reasoning.appendChild(_toggle);
|
||||
|
||||
_toggle.addEventListener("click", () => {
|
||||
this.#expanded = !this.#expanded;
|
||||
|
||||
if (this.#expanded) {
|
||||
this.#_message.classList.add("expanded");
|
||||
} else {
|
||||
this.#_message.classList.remove("expanded");
|
||||
}
|
||||
});
|
||||
|
||||
// message reasoning (content)
|
||||
this.#_reasoning = make("div", "reasoning-text");
|
||||
|
||||
_reasoning.appendChild(this.#_reasoning);
|
||||
|
||||
// message content
|
||||
this.#_text = make("div", "text", "markdown");
|
||||
|
||||
this.#_message.appendChild(this.#_text);
|
||||
|
||||
// message edit textarea
|
||||
this.#_edit = make("textarea", "text");
|
||||
|
||||
this.#_message.appendChild(this.#_edit);
|
||||
|
||||
// message options
|
||||
const _opts = make("div", "options");
|
||||
|
||||
this.#_message.appendChild(_opts);
|
||||
|
||||
// copy option
|
||||
const _optCopy = make("button", "copy");
|
||||
|
||||
_optCopy.title = "Copy message content";
|
||||
|
||||
_opts.appendChild(_optCopy);
|
||||
|
||||
let timeout;
|
||||
|
||||
_optCopy.addEventListener("click", () => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
navigator.clipboard.writeText(this.#text);
|
||||
|
||||
_optCopy.classList.add("copied");
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
_optCopy.classList.remove("copied");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// edit option
|
||||
const _optEdit = make("button", "edit");
|
||||
|
||||
_optEdit.title = "Edit message content";
|
||||
|
||||
_opts.appendChild(_optEdit);
|
||||
|
||||
_optEdit.addEventListener("click", () => {
|
||||
this.toggleEdit();
|
||||
});
|
||||
|
||||
// delete option
|
||||
const _optDelete = make("button", "delete");
|
||||
|
||||
_optDelete.title = "Delete message";
|
||||
|
||||
_opts.appendChild(_optDelete);
|
||||
|
||||
_optDelete.addEventListener("click", () => {
|
||||
this.delete();
|
||||
});
|
||||
|
||||
$messages.appendChild(this.#_message);
|
||||
|
||||
scroll();
|
||||
}
|
||||
|
||||
#render(only = false) {
|
||||
if (!only || only === "role") {
|
||||
this.#_role.textContent = this.#role;
|
||||
}
|
||||
|
||||
if (!only || only === "reasoning") {
|
||||
this.#_reasoning.innerHTML = render(this.#reasoning);
|
||||
|
||||
if (this.#reasoning) {
|
||||
this.#_message.classList.add("has-reasoning");
|
||||
} else {
|
||||
this.#_message.classList.remove("has-reasoning");
|
||||
}
|
||||
}
|
||||
|
||||
if (!only || only === "text") {
|
||||
this.#_text.innerHTML = render(this.#text);
|
||||
}
|
||||
|
||||
scroll();
|
||||
}
|
||||
|
||||
#save() {
|
||||
const data = messages.map((message) => message.getData(true));
|
||||
|
||||
console.log("save", data);
|
||||
|
||||
localStorage.setItem("messages", JSON.stringify(data));
|
||||
}
|
||||
|
||||
getData(includeReasoning = false) {
|
||||
const data = {
|
||||
role: this.#role,
|
||||
text: this.#text,
|
||||
};
|
||||
|
||||
if (this.#reasoning && includeReasoning) {
|
||||
data.reasoning = this.#reasoning;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
if (this.#state === state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#state) {
|
||||
this.#_message.classList.remove(this.#state);
|
||||
}
|
||||
|
||||
if (state) {
|
||||
this.#_message.classList.add(state);
|
||||
}
|
||||
|
||||
this.#state = state;
|
||||
}
|
||||
|
||||
addReasoning(chunk) {
|
||||
this.#reasoning += chunk;
|
||||
|
||||
this.#render("reasoning");
|
||||
this.#save();
|
||||
}
|
||||
|
||||
addText(text) {
|
||||
this.#text += text;
|
||||
|
||||
this.#render("text");
|
||||
this.#save();
|
||||
}
|
||||
|
||||
stopEdit() {
|
||||
if (!this.#editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleEdit();
|
||||
}
|
||||
|
||||
toggleEdit() {
|
||||
this.#editing = !this.#editing;
|
||||
|
||||
if (this.#editing) {
|
||||
this.#_edit.value = this.#text;
|
||||
|
||||
this.#_edit.style.height = `${this.#_text.offsetHeight}px`;
|
||||
this.#_edit.style.width = `${this.#_text.offsetWidth}px`;
|
||||
|
||||
this.setState("editing");
|
||||
|
||||
this.#_edit.focus();
|
||||
} else {
|
||||
this.#text = this.#_edit.value;
|
||||
|
||||
this.setState(false);
|
||||
|
||||
this.#render();
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
const index = messages.findIndex((msg) => msg.#id === this.#id);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("delete", index);
|
||||
|
||||
messages.splice(index, 1);
|
||||
|
||||
this.#_message.remove();
|
||||
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
let controller;
|
||||
|
||||
async function json(url) {
|
||||
@@ -116,134 +391,21 @@
|
||||
function restore(models) {
|
||||
$role.value = localStorage.getItem("role") || "user";
|
||||
$model.value = localStorage.getItem("model") || models[0].id;
|
||||
$prompt.value = localStorage.getItem("prompt") || "normal";
|
||||
$temperature.value = localStorage.getItem("temperature") || 0.85;
|
||||
|
||||
try {
|
||||
const messages = JSON.parse(localStorage.getItem("messages") || "[]");
|
||||
|
||||
messages.forEach(addMessage);
|
||||
JSON.parse(localStorage.getItem("messages") || "[]").forEach(
|
||||
(message) =>
|
||||
new Message(
|
||||
message.role,
|
||||
message.reasoning,
|
||||
message.text,
|
||||
),
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function saveMessages() {
|
||||
localStorage.setItem("messages", JSON.stringify(buildMessages(false)));
|
||||
}
|
||||
|
||||
function scrollMessages() {
|
||||
$messages.scroll({
|
||||
top: $messages.scrollHeight + 200,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
|
||||
function toggleEditing(el) {
|
||||
const text = el.querySelector("div.text"),
|
||||
edit = el.querySelector("textarea.text");
|
||||
|
||||
if (el.classList.contains("editing")) {
|
||||
text.textContent = edit.value.trim();
|
||||
|
||||
el.classList.remove("editing");
|
||||
|
||||
saveMessages();
|
||||
} else {
|
||||
edit.value = text.textContent;
|
||||
edit.style.height = `${text.offsetHeight}px`;
|
||||
|
||||
el.classList.add("editing");
|
||||
|
||||
edit.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(message) {
|
||||
const el = document.createElement("div");
|
||||
|
||||
el.classList.add("message", message.role);
|
||||
|
||||
// message role
|
||||
const role = document.createElement("div");
|
||||
|
||||
role.textContent = message.role;
|
||||
role.classList.add("role");
|
||||
|
||||
el.appendChild(role);
|
||||
|
||||
// message content
|
||||
const text = document.createElement("div");
|
||||
|
||||
text.textContent = message.content;
|
||||
text.classList.add("text");
|
||||
|
||||
el.appendChild(text);
|
||||
|
||||
// message edit textarea
|
||||
const edit = document.createElement("textarea");
|
||||
|
||||
edit.classList.add("text");
|
||||
|
||||
el.appendChild(edit);
|
||||
|
||||
// message options
|
||||
const opts = document.createElement("div");
|
||||
|
||||
opts.classList.add("options");
|
||||
|
||||
el.appendChild(opts);
|
||||
|
||||
// edit option
|
||||
const optEdit = document.createElement("button");
|
||||
|
||||
optEdit.title = "Edit message content";
|
||||
optEdit.classList.add("edit");
|
||||
|
||||
opts.appendChild(optEdit);
|
||||
|
||||
optEdit.addEventListener("click", () => {
|
||||
toggleEditing(el);
|
||||
});
|
||||
|
||||
// delete option
|
||||
const optDelete = document.createElement("button");
|
||||
|
||||
optDelete.title = "Delete message";
|
||||
optDelete.classList.add("delete");
|
||||
|
||||
opts.appendChild(optDelete);
|
||||
|
||||
optDelete.addEventListener("click", () => {
|
||||
el.remove();
|
||||
|
||||
saveMessages();
|
||||
});
|
||||
|
||||
// append to messages
|
||||
$messages.appendChild(el);
|
||||
|
||||
scrollMessages();
|
||||
|
||||
return {
|
||||
set(content) {
|
||||
text.textContent = content;
|
||||
|
||||
scrollMessages();
|
||||
},
|
||||
state(state) {
|
||||
if (state && el.classList.contains(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.classList.remove("waiting", "reasoning", "receiving");
|
||||
|
||||
if (state) {
|
||||
el.classList.add(state);
|
||||
}
|
||||
|
||||
scrollMessages();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function pushMessage() {
|
||||
const text = $message.value.trim();
|
||||
|
||||
@@ -251,40 +413,9 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
addMessage({
|
||||
role: $role.value,
|
||||
content: text,
|
||||
});
|
||||
|
||||
$message.value = "";
|
||||
|
||||
saveMessages();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildMessages(clean = true) {
|
||||
const messages = [];
|
||||
|
||||
$messages.querySelectorAll(".message").forEach((message) => {
|
||||
if (clean && message.classList.contains("editing")) {
|
||||
toggleEditing(message);
|
||||
}
|
||||
|
||||
const role = message.querySelector(".role"),
|
||||
text = message.querySelector(".text");
|
||||
|
||||
if (!role || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: role.textContent.trim(),
|
||||
content: text.textContent.trim().replace(/\r/g, ""),
|
||||
});
|
||||
});
|
||||
|
||||
return messages;
|
||||
return new Message($role.value, "", text);
|
||||
}
|
||||
|
||||
$messages.addEventListener("scroll", () => {
|
||||
@@ -313,6 +444,10 @@
|
||||
localStorage.setItem("model", $model.value);
|
||||
});
|
||||
|
||||
$prompt.addEventListener("change", () => {
|
||||
localStorage.setItem("prompt", $prompt.value);
|
||||
});
|
||||
|
||||
$temperature.addEventListener("input", () => {
|
||||
localStorage.setItem("temperature", $temperature.value);
|
||||
});
|
||||
@@ -330,9 +465,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$messages.innerHTML = "";
|
||||
|
||||
saveMessages();
|
||||
for (const message of messages) {
|
||||
message.delete();
|
||||
}
|
||||
});
|
||||
|
||||
$send.addEventListener("click", () => {
|
||||
@@ -349,26 +484,21 @@
|
||||
}
|
||||
|
||||
pushMessage();
|
||||
saveMessages();
|
||||
|
||||
controller = new AbortController();
|
||||
|
||||
$chat.classList.add("completing");
|
||||
|
||||
const body = {
|
||||
prompt: $prompt.value,
|
||||
model: $model.value,
|
||||
temperature: temperature,
|
||||
messages: buildMessages(),
|
||||
messages: messages.map((message) => message.getData()),
|
||||
};
|
||||
|
||||
const result = {
|
||||
role: "assistant",
|
||||
content: "",
|
||||
};
|
||||
const message = new Message("assistant", "", "");
|
||||
|
||||
const message = addMessage(result);
|
||||
|
||||
message.state("waiting");
|
||||
message.setState("waiting");
|
||||
|
||||
stream(
|
||||
"/-/chat",
|
||||
@@ -384,28 +514,27 @@
|
||||
if (!chunk) {
|
||||
controller = null;
|
||||
|
||||
saveMessages();
|
||||
message.setState(false);
|
||||
|
||||
$chat.classList.remove("completing");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chunk);
|
||||
|
||||
switch (chunk.type) {
|
||||
case "reason":
|
||||
message.state("reasoning");
|
||||
message.setState("reasoning");
|
||||
message.addReasoning(chunk.text);
|
||||
|
||||
break;
|
||||
case "text":
|
||||
result.content += chunk.text;
|
||||
|
||||
message.state("receive");
|
||||
message.set(result.content);
|
||||
message.setState("receiving");
|
||||
message.addText(chunk.text);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
saveMessages();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
BIN
static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
7
static/icons/check.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 579 B |
7
static/icons/chevron.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 568 B |
7
static/icons/copy.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 783 B |
7
static/icons/loading.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -3,5 +3,5 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 894 B |
7
static/icons/prompt.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 685 B |
7
static/icons/reasoning.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 1.2 KiB |
@@ -36,6 +36,13 @@
|
||||
<label for="model" title="Model"></label>
|
||||
<select id="model"></select>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label for="prompt" title="Prompt"></label>
|
||||
<select id="prompt">
|
||||
<option value="" selected>No Prompt</option>
|
||||
<option value="normal">Assistant</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="option">
|
||||
<label for="temperature" title="Temperature (0 - 1)"></label>
|
||||
<input id="temperature" type="number" min="0" max="1" step="0.05" value="0.85" />
|
||||
@@ -47,6 +54,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="purify.min.js"></script>
|
||||
<script src="marked.min.js"></script>
|
||||
<script src="chat.js"></script>
|
||||
</body>
|
||||
</html>
|
BIN
static/logo.png
Normal file
After Width: | Height: | Size: 84 KiB |