commit 3d72105c2c7b2533d65e5b8277245ee31ac23299 Author: dab Date: Mon Aug 18 18:50:33 2025 +0000 initial commit -- llama-cli GUI example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d286b7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +bun.lock diff --git a/entry/run-llamacli-gui.js b/entry/run-llamacli-gui.js new file mode 100644 index 0000000..f2cb96f --- /dev/null +++ b/entry/run-llamacli-gui.js @@ -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' ]) diff --git a/package.json b/package.json new file mode 100644 index 0000000..f40aed7 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "framerock": "git+https://git.daemons.my/dab/framerock.git" + } +} \ No newline at end of file diff --git a/templates/run-process-gui.js b/templates/run-process-gui.js new file mode 100644 index 0000000..5195a85 --- /dev/null +++ b/templates/run-process-gui.js @@ -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', '') + 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 }