441 lines
9.9 KiB
JavaScript
441 lines
9.9 KiB
JavaScript
import ollama from "ollama";
|
|
import blessed from "blessed";
|
|
import { execSync } from "child_process";
|
|
import toml from "toml";
|
|
import fs from "fs";
|
|
|
|
if (!fs.existsSync("./config.toml")) {
|
|
if (fs.existsSync("./config.example.toml")) {
|
|
fs.copyFileSync("./config.example.toml", "./config.toml");
|
|
} else {
|
|
console.error(
|
|
"error: config.example.toml not found. git pull from repository",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const config = toml.parse(fs.readFileSync("./config.toml", "utf-8"));
|
|
|
|
let assistantname = config.assistant.name;
|
|
let assistantface = config.assistant.assistantface;
|
|
let assistantmodel = config.assistant.model;
|
|
let maxtokens = config.advanced.max_tokens;
|
|
let temperature = config.advanced.temperature;
|
|
|
|
let username = config.user.name;
|
|
|
|
let facefont = config.appearance.facefont;
|
|
|
|
let systemprompt = config.assistant.system_prompt
|
|
.replace("${name}", assistantname)
|
|
.replace("${username}", username);
|
|
|
|
const screen = blessed.screen({
|
|
smartCSR: true,
|
|
title: assistantname,
|
|
});
|
|
|
|
const faceBox = blessed.box({
|
|
top: 0,
|
|
left: 1,
|
|
width: "100%",
|
|
height: 9,
|
|
scrollable: false,
|
|
mouse: true,
|
|
keys: true,
|
|
vi: true,
|
|
tags: true,
|
|
content: "",
|
|
});
|
|
|
|
const chatBox = blessed.box({
|
|
top: 9,
|
|
left: 1,
|
|
width: "100%",
|
|
bottom: 3,
|
|
scrollable: true,
|
|
alwaysScroll: true,
|
|
mouse: true,
|
|
keys: true,
|
|
vi: true,
|
|
tags: true,
|
|
content: `go on, tell ${assistantname} something! esc to tab out of chatbox.`,
|
|
});
|
|
|
|
const inputBox = blessed.textbox({
|
|
bottom: 0,
|
|
left: 0,
|
|
width: "100%",
|
|
height: 3,
|
|
border: {
|
|
type: "line",
|
|
},
|
|
style: {
|
|
border: {
|
|
fg: "cyan",
|
|
},
|
|
},
|
|
inputOnFocus: true,
|
|
keys: true,
|
|
mouse: true,
|
|
placeholder: `go on, tell ${assistantname} something!`,
|
|
});
|
|
|
|
const menuBox = blessed.list({
|
|
top: "center",
|
|
left: "center",
|
|
width: 60,
|
|
height: 16,
|
|
border: {
|
|
type: "line",
|
|
},
|
|
style: {
|
|
border: {
|
|
fg: "cyan",
|
|
},
|
|
bg: "black",
|
|
selected: {
|
|
bg: "cyan",
|
|
fg: "black",
|
|
},
|
|
item: {
|
|
fg: "white",
|
|
},
|
|
},
|
|
keys: true,
|
|
vi: true,
|
|
mouse: true,
|
|
hidden: true,
|
|
label: ` ${assistantname} menu `,
|
|
items: ["help & commands", "clear chat history", `exit ${assistantname}`],
|
|
});
|
|
|
|
const popup = blessed.box({
|
|
parent: screen,
|
|
top: "center",
|
|
left: "center",
|
|
width: 40,
|
|
height: 8,
|
|
border: {
|
|
type: "line",
|
|
},
|
|
style: {
|
|
border: {
|
|
fg: "cyan",
|
|
},
|
|
bg: "black",
|
|
},
|
|
tags: true,
|
|
hidden: true,
|
|
content: "",
|
|
});
|
|
|
|
const popupButton = blessed.button({
|
|
parent: popup,
|
|
bottom: 1,
|
|
left: "center",
|
|
width: 8,
|
|
height: 3,
|
|
content: "OK",
|
|
style: {
|
|
bg: "cyan",
|
|
fg: "black",
|
|
focus: {
|
|
bg: "white",
|
|
fg: "black",
|
|
},
|
|
},
|
|
mouse: true,
|
|
keys: true,
|
|
});
|
|
|
|
screen.append(faceBox);
|
|
screen.append(chatBox);
|
|
screen.append(inputBox);
|
|
screen.append(menuBox);
|
|
screen.append(popup);
|
|
|
|
inputBox.focus();
|
|
|
|
let chatHistory = [];
|
|
let conversationHistory = [];
|
|
let currentStreamMessage = "";
|
|
let menuVisible = false;
|
|
|
|
function addMessage(role, content) {
|
|
let message;
|
|
if (role === username) {
|
|
const padding = Math.max(0, chatBox.width - content.length - 5);
|
|
message = " ".repeat(padding) + content + " <\n";
|
|
} else {
|
|
message = `> ${assistantname}: ${content}\n`;
|
|
}
|
|
chatHistory.push(message);
|
|
|
|
chatBox.setContent(chatHistory.join(""));
|
|
chatBox.setScrollPerc(100);
|
|
screen.render();
|
|
}
|
|
|
|
function updateStreamMessage(content) {
|
|
currentStreamMessage = content;
|
|
|
|
// handle the kaomoji
|
|
let letterIndex = -1;
|
|
for (let i = 0; i < content.length - 1; i++) {
|
|
if (/[a-zA-Z]/.test(content[i])) {
|
|
if (content[i] === "I") {
|
|
letterIndex = i;
|
|
break;
|
|
}
|
|
if (/[a-zA-Z]/.test(content[i + 1])) {
|
|
letterIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (letterIndex !== -1) {
|
|
const faceContent = content.substring(0, letterIndex).trim();
|
|
const messageContent = content.substring(letterIndex).trim();
|
|
|
|
// only update face if kaomoji is 3+ characters because sometimes the model wont listen
|
|
if (faceContent.length >= 3) {
|
|
setFaceBoxContent(faceContent);
|
|
}
|
|
|
|
const tempHistory = [...chatHistory];
|
|
tempHistory.push(`> ${assistantname}: ${messageContent}\n`);
|
|
|
|
chatBox.setContent(tempHistory.join(""));
|
|
chatBox.setScrollPerc(100);
|
|
screen.render();
|
|
} else {
|
|
// if no letters found yet, just show the entire thing
|
|
const tempHistory = [...chatHistory];
|
|
tempHistory.push(`> ${assistantname}: ${content}\n`);
|
|
|
|
chatBox.setContent(tempHistory.join(""));
|
|
chatBox.setScrollPerc(100);
|
|
screen.render();
|
|
}
|
|
}
|
|
|
|
function finalizeStreamMessage() {
|
|
if (currentStreamMessage) {
|
|
let letterIndex = -1;
|
|
for (let i = 0; i < currentStreamMessage.length - 1; i++) {
|
|
if (/[a-zA-Z]/.test(currentStreamMessage[i])) {
|
|
// complicated kaomoji splitting garboleum
|
|
if (currentStreamMessage[i] === "I") {
|
|
letterIndex = i;
|
|
break;
|
|
}
|
|
if (/[a-zA-Z]/.test(currentStreamMessage[i + 1])) {
|
|
letterIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let assistantMessage;
|
|
if (letterIndex !== -1) {
|
|
assistantMessage = currentStreamMessage.substring(letterIndex).trim();
|
|
addMessage(assistantname, assistantMessage);
|
|
} else {
|
|
assistantMessage = currentStreamMessage;
|
|
addMessage(assistantname, currentStreamMessage);
|
|
}
|
|
|
|
conversationHistory.push({
|
|
role: "assistant",
|
|
content: assistantMessage,
|
|
});
|
|
|
|
currentStreamMessage = "";
|
|
}
|
|
}
|
|
|
|
function setFaceBoxContent(content) {
|
|
try {
|
|
const figletOutput = execSync(`figlet -f ${facefont} "${content}"`, {
|
|
encoding: "utf8",
|
|
});
|
|
faceBox.setContent(figletOutput);
|
|
} catch (err) {
|
|
faceBox.setContent(content);
|
|
}
|
|
screen.render();
|
|
}
|
|
|
|
function clearChatHistory() {
|
|
chatHistory = [];
|
|
conversationHistory = [];
|
|
chatBox.setContent("");
|
|
screen.render();
|
|
}
|
|
|
|
function showMenu() {
|
|
menuVisible = true;
|
|
menuBox.show();
|
|
menuBox.focus();
|
|
screen.render();
|
|
}
|
|
|
|
function hideMenu() {
|
|
menuVisible = false;
|
|
menuBox.hide();
|
|
inputBox.focus();
|
|
screen.render();
|
|
}
|
|
|
|
function handleMenuSelection() {
|
|
const selectedIndex = menuBox.selected;
|
|
hideMenu();
|
|
|
|
switch (selectedIndex) {
|
|
case 0:
|
|
addMessage(
|
|
assistantname,
|
|
"available commands:\nl!help - if you wanna know what i can do, run this!\nl!clear - clear chat history, if you want me to forget everything, just run this!\nl!face <text> - if you want to force my expression, here you go! not sure i'll be too happy about it though.\nl!prompt <text> - if you want to change how i act, here you go! not sure i'll be too happy about that either.\n\nMenu shortcuts:\nESC - open/close menu\nUp/Down arrows - navigate menu\nEnter - select option\nCtrl+C or q - quit application\n",
|
|
);
|
|
break;
|
|
case 1:
|
|
clearChatHistory();
|
|
addMessage("system", "SYSTEM: Chat history cleared.");
|
|
break;
|
|
case 2:
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
async function sendMessage(message) {
|
|
if (!message.trim()) return;
|
|
|
|
addMessage(username, message);
|
|
|
|
conversationHistory.push({
|
|
role: "user",
|
|
content: message,
|
|
});
|
|
|
|
try {
|
|
currentStreamMessage = "";
|
|
|
|
const response = await ollama.chat({
|
|
model: assistantmodel,
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: systemprompt,
|
|
},
|
|
...conversationHistory,
|
|
],
|
|
stream: true,
|
|
options: {
|
|
num_predict: maxtokens,
|
|
temperature: temperature,
|
|
},
|
|
});
|
|
|
|
for await (const part of response) {
|
|
if (part.message && part.message.content) {
|
|
currentStreamMessage += part.message.content;
|
|
updateStreamMessage(currentStreamMessage);
|
|
}
|
|
}
|
|
|
|
finalizeStreamMessage();
|
|
} catch (error) {
|
|
addMessage(assistantname, `Failed to get response: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
inputBox.on("submit", async (text) => {
|
|
if (text.trim()) {
|
|
inputBox.clearValue();
|
|
inputBox.focus();
|
|
|
|
// command trash
|
|
if (text.trim().startsWith("l!")) {
|
|
const commandParts = text.trim().slice(2).split(" ");
|
|
const command = commandParts[0];
|
|
const args = commandParts.slice(1).join(" ");
|
|
|
|
switch (command) {
|
|
case "help":
|
|
addMessage(
|
|
assistantname,
|
|
"available commands:\nl!help - if you wanna know what i can do, run this!\nl!clear - clear chat history, if you want me to forget everything, just run this!\nl!face <text> - if you want to force my expression, here you go! not sure i'll be too happy about it though.\nl!prompt <text> - if you want to change how i act, here you go! not sure i'll be too happy about that either.\n",
|
|
);
|
|
break;
|
|
case "clear":
|
|
clearChatHistory();
|
|
break;
|
|
case "face":
|
|
if (args.trim()) {
|
|
setFaceBoxContent(args.trim());
|
|
} else {
|
|
setFaceBoxContent("=w=");
|
|
}
|
|
break;
|
|
case "prompt":
|
|
if (args.trim()) {
|
|
systemprompt = args.trim();
|
|
clearChatHistory();
|
|
addMessage("system", "SYSTEM: Prompt set.");
|
|
} else {
|
|
addMessage("system", "SYSTEM: You didn't provide a prompt.");
|
|
}
|
|
break;
|
|
default:
|
|
addMessage(assistantname, `unknown command: ${command}`);
|
|
break;
|
|
}
|
|
} else {
|
|
// message just send it
|
|
await sendMessage(text);
|
|
}
|
|
}
|
|
});
|
|
|
|
screen.key(["q", "C-c"], () => {
|
|
process.exit(0); // KILL lydia!!!!!!!!!!!!!!!!!!!!!!!
|
|
});
|
|
|
|
screen.key(["escape"], () => {
|
|
if (menuVisible) {
|
|
hideMenu();
|
|
} else {
|
|
showMenu();
|
|
}
|
|
});
|
|
|
|
menuBox.on("select", (item, selected) => {
|
|
if (menuVisible) {
|
|
handleMenuSelection();
|
|
}
|
|
});
|
|
|
|
popupButton.on("press", () => {
|
|
popup.hide();
|
|
inputBox.focus();
|
|
screen.render();
|
|
});
|
|
|
|
screen.key(["enter"], () => {
|
|
if (!popup.hidden) {
|
|
popup.hide();
|
|
inputBox.focus();
|
|
screen.render();
|
|
} else if (menuVisible) {
|
|
handleMenuSelection();
|
|
}
|
|
});
|
|
|
|
screen.on("resize", () => {
|
|
screen.render();
|
|
});
|
|
|
|
setFaceBoxContent(assistantface);
|