initial commit -- llama-cli GUI example

This commit is contained in:
dab 2025-08-18 18:50:33 +00:00
commit 3d72105c2c
4 changed files with 121 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
bun.lock

View file

@ -0,0 +1,2 @@
import { run_process_gui } from '../templates/run-process-gui'
run_process_gui({ hostname: '0.0.0.0', page_title: 'framerock examples | Run llama-cli GUI' }, '/home/user/llama.cpp/build/bin', [ './llama-cli', '-m', '/home/user/models/LFM2-350M-Q4_K_M.gguf', '-p', 'I believe the meaning of life is', '-no-cnv' ])

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"framerock": "git+https://git.daemons.my/dab/framerock.git"
}
}

View file

@ -0,0 +1,112 @@
import { async_run } from 'framerock'
const run_process_gui = (runtime_conf, cwd_for_spawn, args_for_spawn) => {
const jsbuild_app_frontend = async function () {
return `
const streams = {}
const elems_terminals = {}
const async_process_stream = async function (stream_id, stream_key, callback) {
for await (let chunk of streams[stream_id][stream_key].readable) {
callback(chunk)
}
return
}
const setup_dom = function () {
document.body.style = 'height: 100%;'
document.head.insertAdjacentHTML('beforeend', '<style type="text/css">html, body{ height: 100%; width: 100%; margin: 0px; padding: 0px;}</style>')
const elem_root = document.createElement('div')
elem_root.style = 'height: 100%; width: 100%; display: flex; font-size: 16px;'
for (let [ stream_key, style ] of [
['stderr', { backgroundColor: '#333' }],
['stdout', { backgroundColor: '#000' }]
]) {
const elem_terminal = document.createElement('div')
elems_terminals[stream_key] = elem_terminal
Object.assign(elem_terminal.style, {
flex: '1', fontFamily: 'monospace', color: '#FFF', height: '100%', overflow: 'auto', padding: '20px', boxSizing: 'border-box', whiteSpace: 'pre',
...style
})
elem_root.appendChild(elem_terminal)
}
document.body.appendChild(elem_root)
return
}
const on_open = function () {
const main_streamid = '1'
streams[main_streamid] = {
prev_message_idx: -1,
stdout: new TextDecoderStream(),
stderr: new TextDecoderStream(),
}
for (let stream_key of ['stdout', 'stderr']) {
async_process_stream(main_streamid, stream_key, (chunk) => {elems_terminals[stream_key].textContent += chunk;}).then(()=>{}).catch(console.error)
}
FRAMEROCK_UTILS.transport_send_bytes(new TextEncoder().encode(JSON.stringify([ 'SPAWN_PROCESS_INIT', { stream_id: main_streamid } ])))
console.info(['Process Spawned'])
return
}
const on_message = function (event) {
const [ evt_type, evt_data ] = JSON.parse(new TextDecoder().decode(event.data))
if (evt_type === 'SPAWN_PROCESS_EVENT') {
const [ stream_id, msg_idx, payload ] = evt_data
const stream = streams[stream_id]
if (stream === undefined) {
console.error(['Unexpected: missing ref to stream:', stream_id])
return
}
if (msg_idx !== stream.prev_message_idx+1) {
console.error('received message out of order, skipping')
return
}
stream.prev_message_idx = msg_idx*1
const [ evt_subtype, evt_subdata ] = payload
if (evt_subtype === 'stream_chunk') {
const [ stream_key, stream_data ] = evt_subdata
if (stream_data === null) {
stream[stream_key].writable.close()
}
else {
const writer = stream[stream_key].writable.getWriter()
writer.write(new Uint8Array(stream_data))
writer.releaseLock()
}
}
else if (evt_subtype === 'exited') {
console.info(['Process Exited', evt_subdata])
}
}
return
}
setup_dom()
FRAMEROCK_UTILS.setup_transport({ on_open, on_message })
`.trim()
}
const handle_transport_bytes = function (utils, message) {
const [ evt_type, evt_data ] = JSON.parse(new TextDecoder().decode(message))
if (evt_type === 'SPAWN_PROCESS_INIT') {
let msg_idx = 0
const wrap_send = (payload) => {
utils.transport_send_bytes(new TextEncoder().encode(JSON.stringify([ 'SPAWN_PROCESS_EVENT', [ evt_data.stream_id, msg_idx*1, payload ] ])))
msg_idx++
return
}
const onExit = (p, exitCode, signalCode, error) => {
wrap_send(['exited', { exitCode, signalCode }])
return
}
const proc = Bun.spawn(args_for_spawn, { cwd: cwd_for_spawn, stdout: 'pipe', stderr: 'pipe', onExit })
const async_handle_chunks = async (stream_key) => {
for await (const chunk of proc[stream_key]) {
wrap_send(['stream_chunk', [ stream_key, [ ...chunk ] ]])
}
wrap_send(['stream_chunk', [ stream_key, null ]])
return
}
Promise.all([ async_handle_chunks('stdout'), async_handle_chunks('stderr') ]).then(()=>{}).catch(console.error)
}
return
}
async_run({ config: { ...runtime_conf }, jsbuild_app_frontend, handle_transport_bytes }).then(()=>{}).catch(console.error)
return
}
export { run_process_gui }