2025-07-18 12:53:00 +02:00
import ollama from "ollama" ;
import blessed from "blessed" ;
import { execSync } from "child_process" ;
import toml from "toml" ;
import fs from "fs" ;
2025-07-18 17:02:52 +02:00
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 ) ;
}
}
2025-07-18 12:53:00 +02:00
const config = toml . parse ( fs . readFileSync ( "./config.toml" , "utf-8" ) ) ;
2025-07-18 22:01:39 +02:00
let assistantname = config . assistant . name ;
let assistantface = config . assistant . assistantface ;
let assistantmodel = config . assistant . model ;
2025-07-18 23:15:02 +02:00
let maxtokens = config . advanced . max _tokens ;
let temperature = config . advanced . temperature ;
2025-07-18 22:01:39 +02:00
let username = config . user . name ;
let facefont = config . appearance . facefont ;
let systemprompt = config . assistant . system _prompt
2025-07-18 12:53:00 +02:00
. replace ( "${name}" , assistantname )
. replace ( "${username}" , username ) ;
const screen = blessed . screen ( {
smartCSR : true ,
title : assistantname ,
} ) ;
const faceBox = blessed . box ( {
top : 0 ,
left : 1 ,
width : "100%" ,
2025-07-19 10:52:52 +02:00
height : 9 ,
scrollable : false ,
2025-07-18 12:53:00 +02:00
mouse : true ,
keys : true ,
vi : true ,
tags : true ,
content : "" ,
} ) ;
const chatBox = blessed . box ( {
top : 9 ,
left : 1 ,
width : "100%" ,
2025-07-19 10:52:52 +02:00
bottom : 3 ,
2025-07-18 12:53:00 +02:00
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! ` ,
} ) ;
2025-08-02 21:44:32 +02:00
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 ,
} ) ;
2025-07-18 12:53:00 +02:00
screen . append ( faceBox ) ;
screen . append ( chatBox ) ;
screen . append ( inputBox ) ;
2025-08-02 21:44:32 +02:00
screen . append ( menuBox ) ;
screen . append ( popup ) ;
2025-07-18 12:53:00 +02:00
inputBox . focus ( ) ;
let chatHistory = [ ] ;
2025-07-18 16:48:11 +02:00
let conversationHistory = [ ] ;
2025-07-18 12:53:00 +02:00
let currentStreamMessage = "" ;
2025-08-02 21:44:32 +02:00
let menuVisible = false ;
2025-07-18 12:53:00 +02:00
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 ;
}
}
}
2025-07-18 16:48:11 +02:00
let assistantMessage ;
2025-07-18 12:53:00 +02:00
if ( letterIndex !== - 1 ) {
2025-07-18 16:48:11 +02:00
assistantMessage = currentStreamMessage . substring ( letterIndex ) . trim ( ) ;
addMessage ( assistantname , assistantMessage ) ;
2025-07-18 12:53:00 +02:00
} else {
2025-07-18 16:48:11 +02:00
assistantMessage = currentStreamMessage ;
2025-07-18 12:53:00 +02:00
addMessage ( assistantname , currentStreamMessage ) ;
}
2025-07-18 16:48:11 +02:00
conversationHistory . push ( {
role : "assistant" ,
content : assistantMessage ,
} ) ;
2025-07-18 12:53:00 +02:00
currentStreamMessage = "" ;
}
}
function setFaceBoxContent ( content ) {
try {
2025-07-18 22:01:39 +02:00
const figletOutput = execSync ( ` figlet -f ${ facefont } " ${ content } " ` , {
2025-07-18 12:53:00 +02:00
encoding : "utf8" ,
} ) ;
faceBox . setContent ( figletOutput ) ;
} catch ( err ) {
faceBox . setContent ( content ) ;
}
screen . render ( ) ;
}
2025-07-18 22:01:39 +02:00
function clearChatHistory ( ) {
chatHistory = [ ] ;
conversationHistory = [ ] ;
chatBox . setContent ( "" ) ;
screen . render ( ) ;
}
2025-08-02 21:44:32 +02:00
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 ) ;
}
}
2025-07-18 12:53:00 +02:00
async function sendMessage ( message ) {
if ( ! message . trim ( ) ) return ;
2025-07-18 16:48:11 +02:00
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 ,
2025-07-18 23:15:02 +02:00
options : {
num _predict : maxtokens ,
temperature : temperature ,
} ,
2025-07-18 16:48:11 +02:00
} ) ;
for await ( const part of response ) {
if ( part . message && part . message . content ) {
currentStreamMessage += part . message . content ;
updateStreamMessage ( currentStreamMessage ) ;
}
2025-07-18 12:53:00 +02:00
}
2025-07-18 16:48:11 +02:00
finalizeStreamMessage ( ) ;
} catch ( error ) {
addMessage ( assistantname , ` Failed to get response: ${ error . message } ` ) ;
2025-07-18 12:53:00 +02:00
}
}
inputBox . on ( "submit" , async ( text ) => {
if ( text . trim ( ) ) {
inputBox . clearValue ( ) ;
inputBox . focus ( ) ;
2025-07-18 16:48:11 +02:00
2025-07-18 17:04:30 +02:00
// command trash
2025-07-18 16:48:11 +02:00
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 ,
2025-07-18 22:01:39 +02:00
"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" ,
2025-07-18 16:48:11 +02:00
) ;
break ;
case "clear" :
2025-07-18 22:01:39 +02:00
clearChatHistory ( ) ;
2025-07-18 16:48:11 +02:00
break ;
case "face" :
if ( args . trim ( ) ) {
setFaceBoxContent ( args . trim ( ) ) ;
} else {
setFaceBoxContent ( "=w=" ) ;
}
break ;
2025-07-18 22:01:39 +02:00
case "prompt" :
if ( args . trim ( ) ) {
systemprompt = args . trim ( ) ;
clearChatHistory ( ) ;
2025-07-18 23:33:37 +02:00
addMessage ( "system" , "SYSTEM: Prompt set." ) ;
2025-07-18 22:01:39 +02:00
} else {
2025-07-18 23:33:37 +02:00
addMessage ( "system" , "SYSTEM: You didn't provide a prompt." ) ;
2025-07-18 22:01:39 +02:00
}
break ;
2025-07-18 16:48:11 +02:00
default :
addMessage ( assistantname , ` unknown command: ${ command } ` ) ;
break ;
}
} else {
2025-07-18 17:04:30 +02:00
// message just send it
2025-07-18 16:48:11 +02:00
await sendMessage ( text ) ;
}
2025-07-18 12:53:00 +02:00
}
} ) ;
2025-07-18 22:01:39 +02:00
screen . key ( [ "q" , "C-c" ] , ( ) => {
2025-07-18 12:53:00 +02:00
process . exit ( 0 ) ; // KILL lydia!!!!!!!!!!!!!!!!!!!!!!!
} ) ;
2025-07-18 22:01:39 +02:00
screen . key ( [ "escape" ] , ( ) => {
2025-08-02 21:44:32 +02:00
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 ( ) ;
}
2025-07-18 22:01:39 +02:00
} ) ;
2025-07-18 12:53:00 +02:00
screen . on ( "resize" , ( ) => {
screen . render ( ) ;
} ) ;
setFaceBoxContent ( assistantface ) ;