node-llama-cpp
Version:
Run AI models locally on your machine with node.js bindings for llama.cpp. Enforce a JSON schema on the model output on the generation level
1 lines • 144 kB
JSON
{"files":[{"path":[".editorconfig"],"content":"root = true\n\n[*]\nindent_style = space\nindent_size = 4\n\n[{*.ts,*.tsx,*.js,*.jsx,*.css,*.scss}]\ninsert_final_newline = true\n\n[{package.json,package-lock.json,manifest.json,electron-builder.json5}]\nindent_size = 2\n\n[*.yml]\nindent_size = 2\n"},{"path":[".gitignore"],"content":"/.idea\n/.vscode\nnode_modules\n.DS_Store\n\n/dist\n/dist-electron\n/release\n/models\n"},{"path":["README.md"],"content":"# Electron + TypeScript + React + Vite + `node-llama-cpp`\nThis template provides a minimal setup to get an Electron app working with TypeScript and `node-llama-cpp`, React with TypeScript for the renderer, and some ESLint rules.\n\n## Get started\nInstall node modules and download the model files used by `node-llama-cpp`:\n```bash\nnpm install\n```\n\nStart the project:\n```bash\nnpm start\n```\n\n> Generated using `npm create node-llama-cpp@latest` ([learn more](https://node-llama-cpp.withcat.ai/guide/))\n"},{"path":["electron","electron-env.d.ts"],"content":"/// <reference types=\"vite-plugin-electron/electron-env\" />\n\ndeclare namespace NodeJS {\n interface ProcessEnv {\n /**\n * The built directory structure\n *\n * ```tree\n * ├─┬─┬ dist\n * │ │ └── index.html\n * │ │\n * │ ├─┬ dist-electron\n * │ │ ├── index.js\n * │ │ └── preload.mjs\n * │\n * ```\n */\n APP_ROOT: string,\n /** /dist/ or /public/ */\n VITE_PUBLIC: string\n }\n}\n\n// Used in Renderer process, expose in `preload.ts`\ninterface Window {\n ipcRenderer: import(\"electron\").IpcRenderer\n}\n"},{"path":["electron","index.ts"],"content":"import {fileURLToPath} from \"node:url\";\nimport path from \"node:path\";\nimport {app, shell, BrowserWindow} from \"electron\";\nimport {registerLlmRpc} from \"./rpc/llmRpc.ts\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// The built directory structure\n//\n// ├─┬─┬ dist\n// │ │ └── index.html\n// │ │\n// │ ├─┬ dist-electron\n// │ │ ├── index.js\n// │ │ └── preload.mjs\n// │\nprocess.env.APP_ROOT = path.join(__dirname, \"..\");\n\nexport const VITE_DEV_SERVER_URL = process.env[\"VITE_DEV_SERVER_URL\"];\nexport const MAIN_DIST = path.join(process.env.APP_ROOT, \"dist-electron\");\nexport const RENDERER_DIST = path.join(process.env.APP_ROOT, \"dist\");\n\nprocess.env.VITE_PUBLIC = VITE_DEV_SERVER_URL\n ? path.join(process.env.APP_ROOT, \"public\")\n : RENDERER_DIST;\n\nlet win: BrowserWindow | null;\n\nfunction createWindow() {\n win = new BrowserWindow({\n icon: path.join(process.env.VITE_PUBLIC, \"electron-vite.svg\"),\n webPreferences: {\n preload: path.join(__dirname, \"preload.mjs\"),\n scrollBounce: true\n },\n width: 1000,\n height: 700\n });\n registerLlmRpc(win);\n\n // open external links in the default browser\n win.webContents.setWindowOpenHandler(({url}) => {\n if (url.startsWith(\"file://\"))\n return {action: \"allow\"};\n\n void shell.openExternal(url);\n return {action: \"deny\"};\n });\n\n // Test active push message to Renderer-process.\n win.webContents.on(\"did-finish-load\", () => {\n win?.webContents.send(\"main-process-message\", (new Date()).toLocaleString());\n });\n\n if (VITE_DEV_SERVER_URL)\n void win.loadURL(VITE_DEV_SERVER_URL);\n else\n void win.loadFile(path.join(RENDERER_DIST, \"index.html\"));\n}\n\n// Quit when all windows are closed, except on macOS. There, it's common\n// for applications and their menu bar to stay active until the user quits\n// explicitly with Cmd + Q.\napp.on(\"window-all-closed\", () => {\n if (process.platform !== \"darwin\") {\n app.quit();\n win = null;\n }\n});\n\napp.on(\"activate\", () => {\n // On OS X it's common to re-create a window in the app when the\n // dock icon is clicked and there are no other windows open.\n if (BrowserWindow.getAllWindows().length === 0) {\n createWindow();\n }\n});\n\napp.whenReady().then(createWindow);\n"},{"path":["electron","llm","modelFunctions.ts"],"content":"import {ChatSessionModelFunctions} from \"node-llama-cpp\";\n// import {defineChatSessionFunction} from \"node-llama-cpp\";\n\nexport const modelFunctions = {\n // getDate: defineChatSessionFunction({\n // description: \"Get the current date\",\n // handler() {\n // const date = new Date();\n // return [\n // date.getFullYear(),\n // String(date.getMonth() + 1).padStart(2, \"0\"),\n // String(date.getDate()).padStart(2, \"0\")\n // ].join(\"-\");\n // }\n // }),\n //\n // getTime: defineChatSessionFunction({\n // description: \"Get the current time\",\n // handler() {\n // return new Date().toLocaleTimeString(\"en-US\");\n // }\n // })\n //\n // getWeather: defineChatSessionFunction({\n // description: \"Get the current weather for a given location\",\n // params: {\n // type: \"object\",\n // properties: {\n // location: {\n // type: \"string\"\n // }\n // }\n // },\n // handler({location}) {\n // return {\n // location,\n // unit: \"celsius\",\n // temperature: 35\n // };\n // }\n // })\n} as const satisfies ChatSessionModelFunctions;\n"},{"path":["electron","preload.ts"],"content":"import {ipcRenderer, contextBridge} from \"electron\";\n\n// --------- Expose some API to the Renderer process ---------\ncontextBridge.exposeInMainWorld(\"ipcRenderer\", {\n on(...args: Parameters<typeof ipcRenderer.on>) {\n const [channel, listener] = args;\n return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args));\n },\n off(...args: Parameters<typeof ipcRenderer.off>) {\n const [channel, ...omit] = args;\n return ipcRenderer.off(channel, ...omit);\n },\n send(...args: Parameters<typeof ipcRenderer.send>) {\n const [channel, ...omit] = args;\n return ipcRenderer.send(channel, ...omit);\n },\n invoke(...args: Parameters<typeof ipcRenderer.invoke>) {\n const [channel, ...omit] = args;\n return ipcRenderer.invoke(channel, ...omit);\n }\n\n // You can expose other APIs you need here\n // ...\n});\n"},{"path":["electron","rpc","llmRpc.ts"],"content":"import path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimport {BrowserWindow, dialog} from \"electron\";\nimport {createElectronSideBirpc} from \"../utils/createElectronSideBirpc.ts\";\nimport {llmFunctions, llmState} from \"../state/llmState.ts\";\nimport type {RenderedFunctions} from \"../../src/rpc/llmRpc.ts\";\n\nconst modelDirectoryPath = path.join(process.cwd(), \"models\");\n\nexport class ElectronLlmRpc {\n public readonly rendererLlmRpc: ReturnType<typeof createElectronSideBirpc<RenderedFunctions, typeof this.functions>>;\n\n public readonly functions = {\n async selectModelFileAndLoad() {\n const res = await dialog.showOpenDialog({\n message: \"Select a model file\",\n title: \"Select a model file\",\n filters: [\n {name: \"Model file\", extensions: [\"gguf\"]}\n ],\n buttonLabel: \"Open\",\n defaultPath: await pathExists(modelDirectoryPath)\n ? modelDirectoryPath\n : undefined,\n properties: [\"openFile\"]\n });\n\n if (!res.canceled && res.filePaths.length > 0) {\n llmState.state = {\n ...llmState.state,\n selectedModelFilePath: path.resolve(res.filePaths[0]!),\n chatSession: {\n loaded: false,\n generatingResult: false,\n simplifiedChat: [],\n draftPrompt: {\n prompt: llmState.state.chatSession.draftPrompt.prompt,\n completion: \"\"\n }\n }\n };\n\n if (!llmState.state.llama.loaded)\n await llmFunctions.loadLlama();\n\n await llmFunctions.loadModel(llmState.state.selectedModelFilePath!);\n await llmFunctions.createContext();\n await llmFunctions.createContextSequence();\n await llmFunctions.chatSession.createChatSession();\n }\n },\n getState() {\n return llmState.state;\n },\n setDraftPrompt: llmFunctions.chatSession.setDraftPrompt,\n prompt: llmFunctions.chatSession.prompt,\n stopActivePrompt: llmFunctions.chatSession.stopActivePrompt,\n resetChatHistory: llmFunctions.chatSession.resetChatHistory\n } as const;\n\n public constructor(window: BrowserWindow) {\n this.rendererLlmRpc = createElectronSideBirpc<RenderedFunctions, typeof this.functions>(\"llmRpc\", \"llmRpc\", window, this.functions);\n\n this.sendCurrentLlmState = this.sendCurrentLlmState.bind(this);\n\n llmState.createChangeListener(this.sendCurrentLlmState);\n this.sendCurrentLlmState();\n }\n\n public sendCurrentLlmState() {\n this.rendererLlmRpc.updateState(llmState.state);\n }\n}\n\nexport type ElectronFunctions = typeof ElectronLlmRpc.prototype.functions;\n\nexport function registerLlmRpc(window: BrowserWindow) {\n new ElectronLlmRpc(window);\n}\n\nasync function pathExists(path: string) {\n try {\n await fs.access(path);\n return true;\n } catch {\n return false;\n }\n}\n"},{"path":["electron","state","llmState.ts"],"content":"import path from \"node:path\";\nimport {\n getLlama, Llama, LlamaChatSession, LlamaChatSessionPromptCompletionEngine, LlamaContext, LlamaContextSequence, LlamaModel,\n isChatModelResponseSegment, type ChatModelSegmentType\n} from \"node-llama-cpp\";\nimport {withLock, State} from \"lifecycle-utils\";\nimport packageJson from \"../../package.json\";\nimport {modelFunctions} from \"../llm/modelFunctions.js\";\n\nexport const llmState = new State<LlmState>({\n appVersion: packageJson.version,\n llama: {\n loaded: false\n },\n model: {\n loaded: false\n },\n context: {\n loaded: false\n },\n contextSequence: {\n loaded: false\n },\n chatSession: {\n loaded: false,\n generatingResult: false,\n simplifiedChat: [],\n draftPrompt: {\n prompt: \"\",\n completion: \"\"\n }\n }\n});\n\nexport type LlmState = {\n appVersion?: string,\n llama: {\n loaded: boolean,\n error?: string\n },\n selectedModelFilePath?: string,\n model: {\n loaded: boolean,\n loadProgress?: number,\n name?: string,\n error?: string\n },\n context: {\n loaded: boolean,\n error?: string\n },\n contextSequence: {\n loaded: boolean,\n error?: string\n },\n chatSession: {\n loaded: boolean,\n generatingResult: boolean,\n simplifiedChat: SimplifiedChatItem[],\n draftPrompt: {\n prompt: string,\n completion: string\n }\n }\n};\n\nexport type SimplifiedChatItem = SimplifiedUserChatItem | SimplifiedModelChatItem;\nexport type SimplifiedUserChatItem = {\n type: \"user\",\n message: string\n};\nexport type SimplifiedModelChatItem = {\n type: \"model\",\n message: Array<{\n type: \"text\",\n text: string\n } | {\n type: \"segment\",\n segmentType: ChatModelSegmentType,\n text: string,\n startTime?: string,\n endTime?: string\n }>\n};\n\nlet llama: Llama | null = null;\nlet model: LlamaModel | null = null;\nlet context: LlamaContext | null = null;\nlet contextSequence: LlamaContextSequence | null = null;\n\nlet chatSession: LlamaChatSession | null = null;\nlet chatSessionCompletionEngine: LlamaChatSessionPromptCompletionEngine | null = null;\nlet promptAbortController: AbortController | null = null;\nlet inProgressResponse: SimplifiedModelChatItem[\"message\"] = [];\n\nexport const llmFunctions = {\n async loadLlama() {\n await withLock([llmFunctions, \"llama\"], async () => {\n if (llama != null) {\n try {\n await llama.dispose();\n llama = null;\n } catch (err) {\n console.error(\"Failed to dispose llama\", err);\n }\n }\n\n try {\n llmState.state = {\n ...llmState.state,\n llama: {loaded: false}\n };\n\n llama = await getLlama();\n llmState.state = {\n ...llmState.state,\n llama: {loaded: true}\n };\n\n llama.onDispose.createListener(() => {\n llmState.state = {\n ...llmState.state,\n llama: {loaded: false}\n };\n });\n } catch (err) {\n console.error(\"Failed to load llama\", err);\n llmState.state = {\n ...llmState.state,\n llama: {\n loaded: false,\n error: String(err)\n }\n };\n }\n });\n },\n async loadModel(modelPath: string) {\n await withLock([llmFunctions, \"model\"], async () => {\n if (llama == null)\n throw new Error(\"Llama not loaded\");\n\n if (model != null) {\n try {\n await model.dispose();\n model = null;\n } catch (err) {\n console.error(\"Failed to dispose model\", err);\n }\n }\n\n try {\n llmState.state = {\n ...llmState.state,\n model: {\n loaded: false,\n loadProgress: 0\n }\n };\n\n model = await llama.loadModel({\n modelPath,\n onLoadProgress(loadProgress: number) {\n llmState.state = {\n ...llmState.state,\n model: {\n ...llmState.state.model,\n loadProgress\n }\n };\n }\n });\n llmState.state = {\n ...llmState.state,\n model: {\n loaded: true,\n loadProgress: 1,\n name: path.basename(modelPath)\n }\n };\n\n model.onDispose.createListener(() => {\n llmState.state = {\n ...llmState.state,\n model: {loaded: false}\n };\n });\n } catch (err) {\n console.error(\"Failed to load model\", err);\n llmState.state = {\n ...llmState.state,\n model: {\n loaded: false,\n error: String(err)\n }\n };\n }\n });\n },\n async createContext() {\n await withLock([llmFunctions, \"context\"], async () => {\n if (model == null)\n throw new Error(\"Model not loaded\");\n\n if (context != null) {\n try {\n await context.dispose();\n context = null;\n } catch (err) {\n console.error(\"Failed to dispose context\", err);\n }\n }\n\n try {\n llmState.state = {\n ...llmState.state,\n context: {loaded: false}\n };\n\n context = await model.createContext();\n llmState.state = {\n ...llmState.state,\n context: {loaded: true}\n };\n\n context.onDispose.createListener(() => {\n llmState.state = {\n ...llmState.state,\n context: {loaded: false}\n };\n });\n } catch (err) {\n console.error(\"Failed to create context\", err);\n llmState.state = {\n ...llmState.state,\n context: {\n loaded: false,\n error: String(err)\n }\n };\n }\n });\n },\n async createContextSequence() {\n await withLock([llmFunctions, \"contextSequence\"], async () => {\n if (context == null)\n throw new Error(\"Context not loaded\");\n\n try {\n llmState.state = {\n ...llmState.state,\n contextSequence: {loaded: false}\n };\n\n contextSequence = context.getSequence();\n llmState.state = {\n ...llmState.state,\n contextSequence: {loaded: true}\n };\n\n contextSequence.onDispose.createListener(() => {\n llmState.state = {\n ...llmState.state,\n contextSequence: {loaded: false}\n };\n });\n } catch (err) {\n console.error(\"Failed to get context sequence\", err);\n llmState.state = {\n ...llmState.state,\n contextSequence: {\n loaded: false,\n error: String(err)\n }\n };\n }\n });\n },\n chatSession: {\n async createChatSession() {\n await withLock([llmFunctions, \"chatSession\"], async () => {\n if (contextSequence == null)\n throw new Error(\"Context sequence not loaded\");\n\n if (chatSession != null) {\n try {\n chatSession.dispose();\n chatSession = null;\n chatSessionCompletionEngine = null;\n } catch (err) {\n console.error(\"Failed to dispose chat session\", err);\n }\n }\n\n try {\n llmState.state = {\n ...llmState.state,\n chatSession: {\n loaded: false,\n generatingResult: false,\n simplifiedChat: [],\n draftPrompt: llmState.state.chatSession.draftPrompt\n }\n };\n\n llmFunctions.chatSession.resetChatHistory(false);\n\n try {\n await chatSession?.preloadPrompt(\"\", {\n functions: modelFunctions, // these won't be called, but are used to avoid redundant context shifts\n signal: promptAbortController?.signal\n });\n } catch (err) {\n // do nothing\n }\n chatSessionCompletionEngine?.complete(llmState.state.chatSession.draftPrompt.prompt);\n\n llmState.state = {\n ...llmState.state,\n chatSession: {\n ...llmState.state.chatSession,\n loaded: true\n }\n };\n } catch (err) {\n console.error(\"Failed to create chat session\", err);\n llmState.state = {\n ...llmState.state,\n chatSession: {\n loaded: false,\n generatingResult: false,\n simplifiedChat: [],\n draftPrompt: llmState.state.chatSession.draftPrompt\n }\n };\n }\n });\n },\n async prompt(message: string) {\n await withLock([llmFunctions, \"chatSession\"], async () => {\n if (chatSession == null)\n throw new Error(\"Chat session not loaded\");\n\n llmState.state = {\n ...llmState.state,\n chatSession: {\n ...llmState.state.chatSession,\n generatingResult: true,\n draftPrompt: {\n prompt: \"\",\n completion: \"\"\n }\n }\n };\n promptAbortController = new AbortController();\n\n llmState.state = {\n ...llmState.state,\n chatSession: {\n ...llmState.state.chatSession,\n simplifiedChat: getSimplifiedChatHistory(true, message)\n }\n };\n\n const abortSignal = promptAbortController.signal;\n try {\n await chatSession.prompt(message, {\n signal: abortSignal,\n stopOnAbortSignal: true,\n functions: modelFunctions,\n onResponseChunk(chunk) {\n inProgressResponse = squashMessageIntoModelChatMessages(\n inProgressResponse,\n (chunk.type == null || chunk.segmentType == null)\n ? {\n type: \"text\",\n text: chunk.text\n }\n : {\n type: \"segment\",\n segmentType: chunk.segmentType,\n text: chunk.text,\n startTime: chunk.segmentStartTime?.toISOString(),\n endTime: chunk.segmentEndTime?.toISOString()\n }\n );\n\n llmState.state = {\n ...llmState.state,\n chatSession: {\n ...llmState.state.chatSession,\n simplifiedChat: getSimplifiedChatHistory(true, message)\n }\n };\n }\n });\n } catch (err) {\n if (err !== abortSignal.reason)\n throw err;\n\n // if the prompt was aborted before the generation even started, we ignore the error\n }\n\n llmState.state = {\n ...llmState.state,\n chatSession: {\n ...llmState.state.chatSession,\n generatingResult: false,\n simplifiedChat: getSimplifiedChatHistory(false),\n draftPrompt: {\n ...llmState.state.chatSession.draftPrompt,\n completion:\n chatSessionCompletionEngine?.complete(llmState.state.chatSession.draftPrompt.prompt)?.trimStart() ?? \"\"\n }\n }\n };\n inProgressResponse = [];\n });\n },\n stopActivePrompt() {\n promptAbortController?.abort();\n },\n resetChatHistory(markAsLoaded: boolean = true) {\n if (contextSequence == null)\n return;\n\n chatSession?.dispose();\n chatSession = new LlamaChatSession({\n contextSequence,\n autoDisposeSequence: false\n });\n chatSessionCompletionEngine = chatSession.createPromptCompletionEngine({\n functions: modelFunctions, // these won't be called, but are used to avoid redundant context shifts\n onGeneration(prompt, completion) {\n if (llmState.state.chatSession.draftPrompt.prompt === prompt) {\n llmState.state = {\n ...llmState.state,\n chatSession: {\n ...llmState.state.chatSession,\n draftPrompt: {\n prompt,\n completion: completion.trimStart()\n }\n }\n };\n }\n }\n });\n\n llmState.state = {\n ...llmState.state,\n chatSession: {\n loaded: markAsLoaded\n ? true\n : llmState.state.chatSession.loaded,\n generatingResult: false,\n simplifiedChat: [],\n draftPrompt: {\n prompt: llmState.state.chatSession.draftPrompt.prompt,\n completion: chatSessionCompletionEngine.complete(llmState.state.chatSession.draftPrompt.prompt)?.trimStart() ?? \"\"\n }\n }\n };\n\n chatSession.onDispose.createListener(() => {\n chatSessionCompletionEngine = null;\n promptAbortController = null;\n llmState.state = {\n ...llmState.state,\n chatSession: {\n loaded: false,\n generatingResult: false,\n simplifiedChat: [],\n draftPrompt: llmState.state.chatSession.draftPrompt\n }\n };\n });\n },\n setDraftPrompt(prompt: string) {\n if (chatSessionCompletionEngine == null)\n return;\n\n llmState.state = {\n ...llmState.state,\n chatSession: {\n ...llmState.state.chatSession,\n draftPrompt: {\n prompt: prompt,\n completion: chatSessionCompletionEngine.complete(prompt)?.trimStart() ?? \"\"\n }\n }\n };\n }\n }\n} as const;\n\nfunction getSimplifiedChatHistory(generatingResult: boolean, currentPrompt?: string) {\n if (chatSession == null)\n return [];\n\n const chatHistory: SimplifiedChatItem[] = chatSession.getChatHistory()\n .flatMap((item): SimplifiedChatItem[] => {\n if (item.type === \"system\")\n return [];\n else if (item.type === \"user\")\n return [{type: \"user\", message: item.text}];\n else if (item.type === \"model\")\n return [{\n type: \"model\",\n message: item.response\n .filter((item) => (typeof item === \"string\" || isChatModelResponseSegment(item)))\n .map((item): SimplifiedModelChatItem[\"message\"][number] | null => {\n if (typeof item === \"string\")\n return {\n type: \"text\",\n text: item\n };\n else if (isChatModelResponseSegment(item))\n return {\n type: \"segment\",\n segmentType: item.segmentType,\n text: item.text,\n startTime: item.startTime,\n endTime: item.endTime\n };\n\n void (item satisfies never); // ensure all item types are handled\n return null;\n })\n .filter((item) => item != null)\n\n // squash adjacent response items of the same type\n .reduce((res, item) => {\n return squashMessageIntoModelChatMessages(res, item);\n }, [] as SimplifiedModelChatItem[\"message\"])\n }];\n\n void (item satisfies never); // ensure all item types are handled\n return [];\n });\n\n if (generatingResult && currentPrompt != null) {\n chatHistory.push({\n type: \"user\",\n message: currentPrompt\n });\n\n if (inProgressResponse.length > 0)\n chatHistory.push({\n type: \"model\",\n message: inProgressResponse\n });\n }\n\n return chatHistory;\n}\n\n/** Squash a new model response message into the existing model response messages array */\nfunction squashMessageIntoModelChatMessages(\n modelChatMessages: SimplifiedModelChatItem[\"message\"],\n message: SimplifiedModelChatItem[\"message\"][number]\n): SimplifiedModelChatItem[\"message\"] {\n const newModelChatMessages = structuredClone(modelChatMessages);\n const lastExistingModelMessage = newModelChatMessages.at(-1);\n\n if (lastExistingModelMessage == null || lastExistingModelMessage.type !== message.type) {\n // avoid pushing empty text messages\n if (message.type !== \"text\" || message.text !== \"\")\n newModelChatMessages.push(message);\n\n return newModelChatMessages;\n }\n\n if (lastExistingModelMessage.type === \"text\" && message.type === \"text\") {\n lastExistingModelMessage.text += message.text;\n return newModelChatMessages;\n } else if (\n lastExistingModelMessage.type === \"segment\" && message.type === \"segment\" &&\n lastExistingModelMessage.segmentType === message.segmentType &&\n lastExistingModelMessage.endTime == null\n ) {\n lastExistingModelMessage.text += message.text;\n lastExistingModelMessage.endTime = message.endTime;\n return newModelChatMessages;\n }\n\n newModelChatMessages.push(message);\n return newModelChatMessages;\n}\n"},{"path":["electron","utils","createElectronSideBirpc.ts"],"content":"import {BrowserWindow, ipcMain} from \"electron\";\nimport {createBirpc} from \"birpc\";\n\nexport function createElectronSideBirpc<\n const RendererFunction = Record<string, never>,\n const ElectronFunctions extends object = Record<string, never>\n>(\n toRendererEventName: string,\n fromRendererEventName: string,\n window: BrowserWindow,\n electronFunctions: ElectronFunctions\n) {\n return createBirpc<RendererFunction, ElectronFunctions>(electronFunctions, {\n post: (data) => window.webContents.send(toRendererEventName, data),\n on: (onData) => ipcMain.on(fromRendererEventName, (event, data) => {\n if (BrowserWindow.fromWebContents(event.sender) === window)\n onData(data);\n }),\n serialize: (value) => JSON.stringify(value),\n deserialize: (value) => JSON.parse(value)\n });\n}\n"},{"path":["electron-builder.ts"],"content":"import path from \"node:path\";\nimport {$} from \"zx\";\nimport type {Configuration} from \"electron-builder\";\n\nconst appId = \"node-llama-cpp.electron.example\";\nconst productName = \"node-llama-cpp Electron example\";\nconst executableName = \"node-llama-cpp-electron-example\";\nconst appxIdentityName = \"node.llama.cpp.electron.example\";\n\n/**\n * @see - https://www.electron.build/configuration/configuration\n */\nexport default {\n appId: appId,\n asar: true,\n productName: productName,\n executableName: executableName,\n directories: {\n output: \"release\"\n },\n icon: \"./public/app-icon.png\",\n\n // remove this once you set up your own code signing for macOS\n async afterPack(context) {\n if (context.electronPlatformName === \"darwin\") {\n // check whether the app was already signed\n const appPath = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`);\n\n // this is needed for the app to not appear as \"damaged\" on Apple Silicon Macs\n // https://github.com/electron-userland/electron-builder/issues/5850#issuecomment-1821648559\n await $`codesign --force --deep --sign - ${appPath}`;\n }\n },\n files: [\n \"dist\",\n \"dist-electron\",\n \"!node_modules/node-llama-cpp/bins/**/*\",\n \"node_modules/node-llama-cpp/bins/${os}-${arch}*/**/*\",\n \"!node_modules/node-llama-cpp/llama/localBuilds/**/*\",\n \"node_modules/node-llama-cpp/llama/localBuilds/${os}-${arch}*/**/*\",\n \"!node_modules/@node-llama-cpp/*/bins/**/*\",\n \"node_modules/@node-llama-cpp/${os}-${arch}*/bins/**/*\"\n ],\n asarUnpack: [\n \"node_modules/node-llama-cpp/bins\",\n \"node_modules/node-llama-cpp/llama/localBuilds\",\n \"node_modules/@node-llama-cpp/*\"\n ],\n mac: {\n target: [{\n target: \"dmg\",\n arch: [\n \"arm64\",\n \"x64\"\n ]\n }, {\n target: \"zip\",\n arch: [\n \"arm64\",\n \"x64\"\n ]\n }],\n\n artifactName: \"${name}.macOS.${version}.${arch}.${ext}\"\n },\n win: {\n target: [{\n target: \"nsis\",\n arch: [\n \"x64\",\n \"arm64\"\n ]\n }],\n\n artifactName: \"${name}.Windows.${version}.${arch}.${ext}\"\n },\n appx: {\n identityName: appxIdentityName,\n artifactName: \"${name}.Windows.${version}.${arch}.${ext}\"\n },\n nsis: {\n oneClick: true,\n perMachine: false,\n allowToChangeInstallationDirectory: false,\n deleteAppDataOnUninstall: true\n },\n linux: {\n target: [{\n target: \"AppImage\",\n arch: [\n \"x64\",\n \"arm64\"\n ]\n }, {\n target: \"snap\",\n arch: [\n \"x64\"\n ]\n }, {\n target: \"deb\",\n arch: [\n \"x64\",\n \"arm64\"\n ]\n }, {\n target: \"tar.gz\",\n arch: [\n \"x64\",\n \"arm64\"\n ]\n }],\n category: \"Utility\",\n\n artifactName: \"${name}.Linux.${version}.${arch}.${ext}\"\n }\n} as Configuration;\n"},{"path":["eslint.config.js"],"content":"// @ts-check\n\nimport importPlugin from \"eslint-plugin-import\";\nimport jsdoc from \"eslint-plugin-jsdoc\";\nimport reactRefresh from \"eslint-plugin-react-refresh\";\nimport tseslint from \"typescript-eslint\";\nimport stylistic from \"@stylistic/eslint-plugin\";\nimport pluginReactHooks from \"eslint-plugin-react-hooks\";\n\n\nexport default tseslint.config({\n ignores: [\"dist/\", \"dist-electron/\", \"release/\", \"models/\"]\n}, {\n files: [\"**/**.{,c,m}{js,ts}{,x}\"],\n extends: [\n stylistic.configs[\"recommended-flat\"],\n jsdoc.configs[\"flat/recommended\"],\n importPlugin.flatConfigs.recommended\n ],\n languageOptions: {\n globals: {\n Atomics: \"readonly\",\n SharedArrayBuffer: \"readonly\"\n },\n\n ecmaVersion: 2023,\n sourceType: \"module\"\n },\n settings: {\n \"import/resolver\": {\n typescript: true,\n node: true\n },\n jsdoc: {\n exemptDestructuredRootsFromChecks: true,\n\n tagNamePreference: {\n hidden: \"hidden\"\n }\n }\n },\n rules: {\n \"@stylistic/indent\": [\"off\"],\n \"indent\": [\"warn\", 4, {\n SwitchCase: 1,\n FunctionDeclaration: {\n parameters: \"first\"\n },\n ignoredNodes: [\n // fix for indent warnings on function object return types when the function has no parameters\n 'FunctionExpression[params.length=0][returnType.type=\"TSTypeAnnotation\"]'\n ]\n }],\n \"@stylistic/indent-binary-ops\": [\"off\"],\n \"@stylistic/eqeqeq\": [\"off\"],\n \"@stylistic/no-undef\": \"off\",\n \"@stylistic/quotes\": [\"warn\", \"double\", {avoidEscape: true}],\n \"no-unused-vars\": [\"warn\", {\n args: \"none\",\n ignoreRestSiblings: true,\n varsIgnorePattern: \"^set\",\n caughtErrors: \"none\"\n }],\n \"@stylistic/no-prototype-builtins\": [\"off\"],\n \"@stylistic/object-curly-spacing\": [\"warn\", \"never\"],\n \"@stylistic/semi\": [\"warn\", \"always\"],\n \"@stylistic/no-undefined\": [\"off\"],\n \"@stylistic/array-bracket-newline\": [\"error\", \"consistent\"],\n \"@stylistic/brace-style\": [\"error\", \"1tbs\", {\n allowSingleLine: false\n }],\n \"@stylistic/comma-spacing\": [\"error\", {\n before: false,\n after: true\n }],\n \"@stylistic/comma-style\": [\"error\", \"last\"],\n \"@stylistic/comma-dangle\": [\"warn\", \"never\"],\n \"no-var\": [\"error\"],\n \"import/order\": [\"error\", {\n groups: [\"builtin\", \"external\", \"internal\", \"parent\", \"sibling\", \"index\", \"type\", \"object\", \"unknown\"],\n warnOnUnassignedImports: true\n }],\n \"newline-per-chained-call\": [\"error\", {\n ignoreChainWithDepth: 2\n }],\n \"no-confusing-arrow\": [\"error\"],\n \"no-const-assign\": [\"error\"],\n \"no-duplicate-imports\": [\"error\", {\n includeExports: true\n }],\n camelcase: [\"warn\"],\n \"@stylistic/jsx-quotes\": [\"warn\"],\n yoda: [\"error\", \"never\", {\n exceptRange: true\n }],\n \"no-eval\": [\"error\"],\n \"array-callback-return\": [\"error\"],\n \"no-empty\": [\"error\", {\n allowEmptyCatch: true\n }],\n \"@stylistic/keyword-spacing\": [\"warn\"],\n \"@stylistic/space-infix-ops\": [\"warn\"],\n \"@stylistic/spaced-comment\": [\"warn\", \"always\", {\n markers: [\"/\"]\n }],\n \"@stylistic/eol-last\": [\"warn\", \"always\"],\n \"@stylistic/max-len\": [\"warn\", {\n code: 140,\n tabWidth: 4,\n ignoreStrings: true\n }],\n \"@stylistic/quote-props\": [\"off\"],\n \"@stylistic/arrow-parens\": [\"warn\", \"always\"],\n \"@stylistic/no-multiple-empty-lines\": [\"off\"],\n \"@stylistic/operator-linebreak\": [\"off\"],\n \"@stylistic/block-spacing\": [\"warn\", \"never\"],\n \"@stylistic/no-extra-parens\": [\"off\"],\n \"@stylistic/padded-blocks\": [\"warn\"],\n \"@stylistic/multiline-ternary\": [\"off\"],\n \"@stylistic/lines-between-class-members\": [\"warn\", {\n enforce: [\n {blankLine: \"always\", prev: \"method\", next: \"*\"},\n {blankLine: \"always\", prev: \"*\", next: \"method\"}\n ]\n }],\n \"@stylistic/no-trailing-spaces\": [\"off\"],\n \"@stylistic/no-multi-spaces\": [\"warn\"],\n \"@stylistic/generator-star-spacing\": [\"off\"]\n }\n}, {\n files: [\"**/**.{ts,tsx}\"],\n extends: [\n jsdoc.configs[\"flat/recommended-typescript\"],\n ...tseslint.configs.recommended\n ],\n plugins: {\n \"react-hooks\": pluginReactHooks,\n \"react-refresh\": reactRefresh\n },\n settings: {\n \"import/resolver\": {\n typescript: true,\n node: true\n }\n },\n rules: {\n ...pluginReactHooks.configs.recommended.rules,\n \"no-constant-condition\": [\"warn\"],\n \"import/named\": [\"off\"],\n \"@typescript-eslint/explicit-module-boundary-types\": [\"off\"],\n \"@typescript-eslint/ban-ts-comment\": [\"off\"],\n \"@typescript-eslint/no-explicit-any\": [\"off\"],\n \"@typescript-eslint/no-inferrable-types\": [\"off\"],\n \"@typescript-eslint/no-unused-vars\": [\"warn\", {\n args: \"none\",\n ignoreRestSiblings: true,\n varsIgnorePattern: \"^set\",\n caughtErrors: \"none\"\n }],\n \"@typescript-eslint/no-empty-object-type\": [\"off\"],\n \"@typescript-eslint/member-ordering\": [\"warn\", {\n default: [\"field\", \"constructor\", \"method\", \"signature\"],\n typeLiterals: []\n }],\n \"@typescript-eslint/parameter-properties\": [\"warn\", {\n allow: []\n }],\n \"@typescript-eslint/explicit-member-accessibility\": [\"warn\"],\n \"@stylistic/member-delimiter-style\": [\"warn\", {\n multiline: {\n delimiter: \"comma\",\n requireLast: false\n },\n singleline: {\n delimiter: \"comma\",\n requireLast: false\n },\n multilineDetection: \"brackets\"\n }],\n \"@stylistic/jsx-wrap-multilines\": [\"off\"],\n \"@stylistic/jsx-indent-props\": [\"warn\", 4],\n \"@stylistic/jsx-one-expression-per-line\": [\"off\"],\n \"@stylistic/jsx-closing-tag-location\": [\"warn\", \"line-aligned\"],\n \"@stylistic/jsx-closing-bracket-location\": [\"warn\", \"line-aligned\"],\n \"@stylistic/jsx-tag-spacing\": [\"warn\"],\n\n \"jsdoc/require-param\": [\"off\"],\n \"jsdoc/check-param-names\": [\"warn\", {\n checkDestructured: false\n }],\n \"jsdoc/require-returns\": [\"off\"],\n \"jsdoc/require-jsdoc\": [\"off\"],\n \"jsdoc/require-yields\": [\"off\"],\n \"jsdoc/require-param-description\": [\"off\"],\n\n \"react-refresh/only-export-components\": [\"warn\", {\n \"allowConstantExport\": true\n }],\n \"react-hooks/exhaustive-deps\": [\"off\"]\n }\n});\n"},{"path":["package.json"],"content":"{\n \"name\": \"node-llama-cpp-project\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"main\": \"./dist-electron/index.js\",\n \"type\": \"module\",\n \"homepage\": \"https://github.com/withcatai/node-llama-cpp\",\n \"author\": {\n \"name\": \"Author name\",\n \"email\": \"email@example.com\"\n },\n \"scripts\": {\n \"postinstall\": \"npm run models:pull\",\n \"models:pull\": \"node-llama-cpp pull --dir ./models \\\"{{modelUriOrUrl|escape|escape}}\\\"\",\n \"start\": \"vite dev\",\n \"start:inspect\": \"cross-env ENABLE_INSPECT=true vite dev\",\n \"start:build\": \"electron ./dist-electron\",\n \"prebuild\": \"rimraf ./dist ./dist-electron ./release\",\n \"build\": \"tsc && vite build && electron-builder --config ./electron-builder.ts\",\n \"lint\": \"npm run lint:eslint\",\n \"lint:eslint\": \"eslint --report-unused-disable-directives .\",\n \"format\": \"npm run lint:eslint -- --fix\",\n \"clean\": \"rm -rf ./node_modules ./dist ./dist-electron ./release ./models\"\n },\n \"dependencies\": {\n \"@fontsource-variable/inter\": \"^5.2.5\",\n \"birpc\": \"^2.3.0\",\n \"classnames\": \"^2.5.1\",\n \"highlight.js\": \"^11.11.1\",\n \"lifecycle-utils\": \"^3.0.1\",\n \"markdown-it\": \"^14.1.0\",\n \"node-llama-cpp\": \"^{{currentNodeLlamaCppModuleVersion|escape}}\",\n \"pretty-ms\": \"^9.2.0\",\n \"react\": \"^19.1.0\",\n \"react-dom\": \"^19.1.0\",\n \"semver\": \"^7.7.1\"\n },\n \"devDependencies\": {\n \"@stylistic/eslint-plugin\": \"^4.2.0\",\n \"@types/markdown-it\": \"^14.1.2\",\n \"@types/react\": \"^19.1.3\",\n \"@types/react-dom\": \"^19.1.3\",\n \"@types/semver\": \"^7.7.0\",\n \"@vitejs/plugin-react\": \"^4.4.1\",\n \"cross-env\": \"^10.0.0\",\n \"electron\": \"^36.2.0\",\n \"electron-builder\": \"^26.0.12\",\n \"eslint\": \"^9.26.0\",\n \"eslint-import-resolver-typescript\": \"^4.3.4\",\n \"eslint-plugin-import\": \"^2.31.0\",\n \"eslint-plugin-jsdoc\": \"^50.6.11\",\n \"eslint-plugin-react-hooks\": \"^5.2.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.20\",\n \"rimraf\": \"^6.0.1\",\n \"typescript\": \"^5.8.3\",\n \"typescript-eslint\": \"^8.32.0\",\n \"vite\": \"^6.3.5\",\n \"vite-plugin-electron\": \"^0.29.0\",\n \"vite-plugin-electron-renderer\": \"^0.14.6\",\n \"zx\": \"^8.5.3\"\n },\n \"overrides\": {\n \"electron-builder\": {\n \"read-config-file\": {\n \"config-file-ts\": \">=0.2.8-rc1\"\n }\n }\n }\n}"},{"path":["public","vite.svg"],"content":"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" aria-hidden=\"true\" role=\"img\"\n class=\"iconify iconify--logos\" width=\"31.88\" height=\"32\" preserveAspectRatio=\"xMidYMid meet\" viewBox=\"0 0 256 257\">\n <defs>\n <linearGradient id=\"IconifyId1813088fe1fbc01fb466\" x1=\"-.828%\" x2=\"57.636%\" y1=\"7.652%\" y2=\"78.411%\">\n <stop offset=\"0%\" stop-color=\"#41D1FF\"></stop>\n <stop offset=\"100%\" stop-color=\"#BD34FE\"></stop>\n </linearGradient>\n <linearGradient id=\"IconifyId1813088fe1fbc01fb467\" x1=\"43.376%\" x2=\"50.316%\" y1=\"2.242%\" y2=\"89.03%\">\n <stop offset=\"0%\" stop-color=\"#FFEA83\"></stop>\n <stop offset=\"8.333%\" stop-color=\"#FFDD35\"></stop>\n <stop offset=\"100%\" stop-color=\"#FFA800\"></stop>\n </linearGradient>\n </defs>\n <path fill=\"url(#IconifyId1813088fe1fbc01fb466)\"\n d=\"M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z\"></path>\n <path fill=\"url(#IconifyId1813088fe1fbc01fb467)\"\n d=\"M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z\"></path>\n</svg>\n"},{"path":["src","App","App.css"],"content":"#root {\n margin: 0 auto;\n padding: 16px;\n text-align: center;\n width: 100%;\n min-height: 100%;\n align-items: center;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n}\n\n.app {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-height: 100%;\n max-width: 1280px;\n\n > .chatHistory {\n margin-bottom: 32px;\n }\n\n > .message {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: space-evenly;\n align-items: center;\n gap: 48px;\n overflow: auto;\n padding: 24px 0px;\n\n > .error {\n border: solid 1px var(--error-border-color);\n padding: 8px 12px;\n border-radius: 12px;\n box-shadow: 0px 8px 32px -16px var(--error-border-color);\n }\n\n > .loadModel {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 64px;\n text-align: start;\n\n > .hint {\n opacity: 0.6;\n }\n\n > .actions {\n display: flex;\n flex-direction: column;\n align-items: center;\n background-color: var(--actions-block-background-color);\n border: solid 1px var(--actions-block-border-color);\n box-shadow: var(--actions-block-box-shadow);\n padding: 16px 24px;\n border-radius: 12px;\n gap: 16px;\n\n > .starLink {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: