node-llama-cpp 3.15.1 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/bindings/AddonTypes.d.ts +8 -1
  2. package/dist/bindings/getLlama.d.ts +1 -1
  3. package/dist/bindings/getLlama.js +1 -1
  4. package/dist/bindings/getLlama.js.map +1 -1
  5. package/dist/chatWrappers/generic/utils/extractFunctionCallSettingsFromJinjaTemplate.js +67 -8
  6. package/dist/chatWrappers/generic/utils/extractFunctionCallSettingsFromJinjaTemplate.js.map +1 -1
  7. package/dist/chatWrappers/generic/utils/extractSegmentSettingsFromTokenizerAndChatTemplate.js +2 -1
  8. package/dist/chatWrappers/generic/utils/extractSegmentSettingsFromTokenizerAndChatTemplate.js.map +1 -1
  9. package/dist/cli/commands/ChatCommand.d.ts +6 -0
  10. package/dist/cli/commands/ChatCommand.js +66 -3
  11. package/dist/cli/commands/ChatCommand.js.map +1 -1
  12. package/dist/cli/commands/CompleteCommand.d.ts +6 -0
  13. package/dist/cli/commands/CompleteCommand.js +66 -4
  14. package/dist/cli/commands/CompleteCommand.js.map +1 -1
  15. package/dist/cli/commands/InfillCommand.d.ts +6 -0
  16. package/dist/cli/commands/InfillCommand.js +66 -4
  17. package/dist/cli/commands/InfillCommand.js.map +1 -1
  18. package/dist/cli/utils/parseXtcArg.d.ts +5 -0
  19. package/dist/cli/utils/parseXtcArg.js +16 -0
  20. package/dist/cli/utils/parseXtcArg.js.map +1 -0
  21. package/dist/evaluator/LlamaChat/LlamaChat.d.ts +36 -1
  22. package/dist/evaluator/LlamaChat/LlamaChat.js +29 -10
  23. package/dist/evaluator/LlamaChat/LlamaChat.js.map +1 -1
  24. package/dist/evaluator/LlamaChatSession/LlamaChatSession.d.ts +83 -2
  25. package/dist/evaluator/LlamaChatSession/LlamaChatSession.js +11 -5
  26. package/dist/evaluator/LlamaChatSession/LlamaChatSession.js.map +1 -1
  27. package/dist/evaluator/LlamaChatSession/utils/LlamaChatSessionPromptCompletionEngine.d.ts +2 -0
  28. package/dist/evaluator/LlamaChatSession/utils/LlamaChatSessionPromptCompletionEngine.js.map +1 -1
  29. package/dist/evaluator/LlamaCompletion.d.ts +36 -3
  30. package/dist/evaluator/LlamaCompletion.js +7 -4
  31. package/dist/evaluator/LlamaCompletion.js.map +1 -1
  32. package/dist/evaluator/LlamaContext/LlamaContext.js +67 -35
  33. package/dist/evaluator/LlamaContext/LlamaContext.js.map +1 -1
  34. package/dist/evaluator/LlamaContext/LlamaSampler.js +8 -0
  35. package/dist/evaluator/LlamaContext/LlamaSampler.js.map +1 -1
  36. package/dist/evaluator/LlamaContext/tokenPredictors/DraftSequenceTokenPredictor.d.ts +1 -1
  37. package/dist/evaluator/LlamaContext/types.d.ts +113 -0
  38. package/dist/evaluator/LlamaModel/LlamaModel.d.ts +2 -2
  39. package/dist/evaluator/LlamaModel/LlamaModel.js +1 -1
  40. package/dist/evaluator/LlamaModel/LlamaModel.js.map +1 -1
  41. package/dist/gguf/insights/GgufInsights.js +4 -0
  42. package/dist/gguf/insights/GgufInsights.js.map +1 -1
  43. package/dist/gguf/types/GgufMetadataTypes.d.ts +5 -0
  44. package/dist/gguf/types/GgufMetadataTypes.js +5 -0
  45. package/dist/gguf/types/GgufMetadataTypes.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/dist/types.d.ts +51 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/utils/cmake.js +6 -3
  50. package/dist/utils/cmake.js.map +1 -1
  51. package/llama/addon/AddonContext.cpp +19 -5
  52. package/llama/addon/AddonContext.h +1 -1
  53. package/llama/addon/AddonSampler.cpp +158 -0
  54. package/llama/addon/AddonSampler.h +13 -1
  55. package/llama/addon/globals/getGpuInfo.cpp +1 -1
  56. package/llama/binariesGithubRelease.json +1 -1
  57. package/llama/gitRelease.bundle +0 -0
  58. package/llama/gpuInfo/vulkan-gpu-info.cpp +12 -5
  59. package/llama/llama.cpp.info.json +1 -1
  60. package/package.json +63 -62
  61. package/templates/packed/electron-typescript-react.json +1 -1
  62. package/templates/packed/node-typescript.json +1 -1
@@ -1 +1 @@
1
- {"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}]\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 --app-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: 8px;\n color: var(--star-link-color);\n\n &:hover {\n color: var(--star-hover-color);\n }\n\n > .starIcon {\n flex-shrink: 0;\n fill: currentColor;\n }\n }\n\n > .separator {\n height: 1px;\n background-color: var(--actions-block-border-color);\n margin: 0px -24px;\n align-self: stretch;\n }\n\n > .title {\n padding-inline-end: 16px;\n color: var(--text-color);\n opacity: 0.8;\n font-weight: 600;\n }\n\n > .links {\n display: flex;\n flex-direction: row;\n\n > a {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n > .downloadIcon {\n flex-shrink: 0;\n fill: currentColor;\n }\n }\n\n > .separator {\n width: 1px;\n background-color: var(--link-color);\n opacity: 0.2;\n margin: 0px 16px;\n height: 0.8lh;\n align-self: center;\n }\n }\n\n > .browseLink {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n > .searchIcon {\n flex-shrink: 0;\n fill: currentColor;\n }\n }\n }\n }\n\n > .loading {\n opacity: 0.6;\n font-weight: bold;\n\n mask: linear-gradient(\n to right,\n rgb(0 0 0 / 48%) 34%,\n black,\n rgb(0 0 0 / 48%) 66%\n ) content-box 0 0 / 300% 100% no-repeat;\n animation: loading-animation 2s infinite ease-in-out;\n }\n\n > .typeMessage {\n opacity: 0.6;\n }\n }\n}\n\n@keyframes loading-animation {\n 0% {\n mask-position: 100% 100%;\n }\n\n 100% {\n mask-position: 0 100%;\n }\n}\n"},{"path":["src","App","App.tsx"],"content":"import {useCallback, useLayoutEffect, useRef} from \"react\";\nimport {llmState} from \"../state/llmState.ts\";\nimport {electronLlmRpc} from \"../rpc/llmRpc.ts\";\nimport {useExternalState} from \"../hooks/useExternalState.ts\";\nimport {SearchIconSVG} from \"../icons/SearchIconSVG.tsx\";\nimport {StarIconSVG} from \"../icons/StarIconSVG.tsx\";\nimport {DownloadIconSVG} from \"../icons/DownloadIconSVG.tsx\";\nimport {Header} from \"./components/Header/Header.tsx\";\nimport {ChatHistory} from \"./components/ChatHistory/ChatHistory.tsx\";\nimport {InputRow} from \"./components/InputRow/InputRow.tsx\";\n\nimport \"./App.css\";\n\n\nexport function App() {\n const state = useExternalState(llmState);\n const {generatingResult} = state.chatSession;\n const isScrollAnchoredRef = useRef(false);\n const lastAnchorScrollTopRef = useRef<number>(0);\n\n const isScrolledToTheBottom = useCallback(() => {\n return (\n document.documentElement.scrollHeight - document.documentElement.scrollTop - 1\n ) <= document.documentElement.clientHeight;\n }, []);\n\n const scrollToBottom = useCallback(() => {\n const newScrollTop = document.documentElement.scrollHeight - document.documentElement.clientHeight;\n\n if (newScrollTop > document.documentElement.scrollTop && newScrollTop > lastAnchorScrollTopRef.current) {\n document.documentElement.scrollTo({\n top: newScrollTop,\n behavior: \"smooth\"\n });\n lastAnchorScrollTopRef.current = document.documentElement.scrollTop;\n }\n\n isScrollAnchoredRef.current = true;\n }, []);\n\n useLayoutEffect(() => {\n // anchor scroll to bottom\n\n function onScroll() {\n const currentScrollTop = document.documentElement.scrollTop;\n\n isScrollAnchoredRef.current = isScrolledToTheBottom() ||\n currentScrollTop >= lastAnchorScrollTopRef.current;\n\n // handle scroll animation\n if (isScrollAnchoredRef.current)\n lastAnchorScrollTopRef.current = currentScrollTop;\n }\n\n const observer = new ResizeObserver(() => {\n if (isScrollAnchoredRef.current && !isScrolledToTheBottom())\n scrollToBottom();\n });\n\n window.addEventListener(\"scroll\", onScroll, {passive: false});\n observer.observe(document.body, {\n box: \"border-box\"\n });\n scrollToBottom();\n\n return () => {\n observer.disconnect();\n window.removeEventListener(\"scroll\", onScroll);\n };\n }, []);\n\n const openSelectModelFileDialog = useCallback(async () => {\n await electronLlmRpc.selectModelFileAndLoad();\n }, []);\n\n const stopActivePrompt = useCallback(() => {\n void electronLlmRpc.stopActivePrompt();\n }, []);\n\n const resetChatHistory = useCallback(() => {\n void electronLlmRpc.stopActivePrompt();\n void electronLlmRpc.resetChatHistory();\n }, []);\n\n const sendPrompt = useCallback((prompt: string) => {\n if (generatingResult)\n return;\n\n scrollToBottom();\n void electronLlmRpc.prompt(prompt);\n }, [generatingResult, scrollToBottom]);\n\n const onPromptInput = useCallback((currentText: string) => {\n void electronLlmRpc.setDraftPrompt(currentText);\n }, []);\n\n const error = state.llama.error ?? state.model.error ?? state.context.error ?? state.contextSequence.error;\n const loading = state.selectedModelFilePath != null && error == null && (\n !state.model.loaded || !state.llama.loaded || !state.context.loaded || !state.contextSequence.loaded || !state.chatSession.loaded\n );\n const showMessage = state.selectedModelFilePath == null || error != null || state.chatSession.simplifiedChat.length === 0;\n\n return <div className=\"app\">\n <Header\n appVersion={state.appVersion}\n canShowCurrentVersion={state.selectedModelFilePath == null}\n modelName={state.model.name}\n loadPercentage={state.model.loadProgress}\n onLoadClick={openSelectModelFileDialog}\n onResetChatClick={\n !showMessage\n ? resetChatHistory\n : undefined\n }\n />\n {\n showMessage &&\n <div className=\"message\">\n {\n error != null &&\n <div className=\"error\">\n {String(error)}\n </div>\n }\n {\n loading &&\n <div className=\"loading\">\n Loading...\n </div>\n }\n {\n (state.selectedModelFilePath == null || state.llama.error != null) &&\n <div className=\"loadModel\">\n <div className=\"hint\">Click the button above to load a model</div>\n <div className=\"actions\">\n <a className=\"starLink\" target=\"_blank\" href=\"https://github.com/withcatai/node-llama-cpp\">\n <StarIconSVG className=\"starIcon\" />\n <div className=\"text\">\n Star <code>node-llama-cpp</code> on GitHub\n </div>\n </a>\n\n <div className=\"separator\"></div>\n <div className=\"title\">DeepSeek R1 Distill Qwen model</div>\n <div className=\"links\">\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/mradermacher/DeepSeek-R1-Distill-Qwen-7B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-7B.Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get 7B</div>\n </a>\n <div className=\"separator\" />\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/mradermacher/DeepSeek-R1-Distill-Qwen-14B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-14B.Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get 14B</div>\n </a>\n <div className=\"separator\" />\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/mradermacher/DeepSeek-R1-Distill-Qwen-32B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-32B.Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get 32B</div>\n </a>\n </div>\n\n <div className=\"separator\"></div>\n <div className=\"title\">Other models</div>\n <div className=\"links\">\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/giladgd/gpt-oss-20b-GGUF/resolve/main/gpt-oss-20b.MXFP4.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\"><code>gpt-oss</code> 20B</div>\n </a>\n <div className=\"separator\" />\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get Gemma 2 2B</div>\n </a>\n </div>\n\n <div className=\"separator\"></div>\n <a className=\"browseLink\" target=\"_blank\" href=\"https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending\">\n <SearchIconSVG className=\"searchIcon\" />\n <div className=\"text\">Find more models</div>\n </a>\n </div>\n </div>\n }\n {\n (\n !loading &&\n state.selectedModelFilePath != null &&\n error == null &&\n state.chatSession.simplifiedChat.length === 0\n ) &&\n <div className=\"typeMessage\">\n Type a message to start the conversation\n </div>\n }\n </div>\n }\n {\n !showMessage &&\n <ChatHistory\n className=\"chatHistory\"\n simplifiedChat={state.chatSession.simplifiedChat}\n generatingResult={generatingResult}\n />\n }\n <InputRow\n disabled={!state.model.loaded || !state.contextSequence.loaded}\n stopGeneration={\n generatingResult\n ? stopActivePrompt\n : undefined\n }\n onPromptInput={onPromptInput}\n sendPrompt={sendPrompt}\n generatingResult={generatingResult}\n autocompleteInputDraft={state.chatSession.draftPrompt.prompt}\n autocompleteCompletion={state.chatSession.draftPrompt.completion}\n />\n </div>;\n}\n"},{"path":["src","App","components","ChatHistory","ChatHistory.css"],"content":".appChatHistory {\n flex: 1;\n display: flex;\n flex-direction: column;\n text-align: start;\n overflow: auto;\n padding: 24px 0px;\n}\n"},{"path":["src","App","components","ChatHistory","ChatHistory.tsx"],"content":"import {useMemo} from \"react\";\nimport classNames from \"classnames\";\nimport {LlmState, SimplifiedModelChatItem} from \"../../../../electron/state/llmState.ts\";\nimport {UserMessage} from \"./components/UserMessage/UserMessage.js\";\nimport {ModelMessage} from \"./components/ModelMessage/ModelMessage.js\";\n\nimport \"./ChatHistory.css\";\n\n\nexport function ChatHistory({simplifiedChat, generatingResult, className}: ChatHistoryProps) {\n const renderChatItems = useMemo(() => {\n if (simplifiedChat.length > 0 &&\n simplifiedChat.at(-1)!.type !== \"model\" &&\n generatingResult\n )\n return [...simplifiedChat, emptyModelMessage];\n\n return simplifiedChat;\n }, [simplifiedChat, generatingResult]);\n\n return <div className={classNames(\"appChatHistory\", className)}>\n {\n renderChatItems\n .map((item, index) => {\n if (item.type === \"model\")\n return <ModelMessage\n key={index}\n modelMessage={item}\n active={index === renderChatItems.length - 1 && generatingResult}\n />;\n else if (item.type === \"user\")\n return <UserMessage key={index} message={item} />;\n\n return null;\n })\n }\n </div>;\n}\n\ntype ChatHistoryProps = {\n simplifiedChat: LlmState[\"chatSession\"][\"simplifiedChat\"],\n generatingResult: boolean,\n className?: string\n};\n\nconst emptyModelMessage: SimplifiedModelChatItem = {\n type: \"model\",\n message: []\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","ModelMessage.css"],"content":".appChatHistory > .message.model {\n align-self: flex-start;\n margin-inline-end: 48px;\n padding-inline-start: 18px;\n word-break: break-word;\n max-width: calc(100% - 48px);\n box-sizing: border-box;\n min-height: fit-content;\n interpolate-size: allow-keywords;\n\n transition: min-height 0.5s var(--transition-easing), max-height 0.5s var(--transition-easing);\n\n &:hover + .buttons {\n opacity: 1;\n }\n\n &:last-child {\n margin-bottom: 0px;\n min-height: calc(50svh);\n\n @starting-style {\n min-height: 0px;\n }\n }\n\n > .text {\n padding: 0px 6px;\n }\n\n > .buttons {\n display: flex;\n flex-direction: row;\n padding: 12px 0px 8px 0px;\n opacity: 0.6;\n justify-self: flex-start;\n\n transition: opacity 0.1s ease-in-out;\n\n &:hover,\n &:focus-visible {\n opacity: 1;\n }\n\n &[inert] {\n opacity: 0;\n }\n }\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","ModelMessage.tsx"],"content":"import {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {SimplifiedModelChatItem} from \"../../../../../../electron/state/llmState.js\";\nimport {ModelResponseThought} from \"../ModelResponseThought/ModelResponseThought.js\";\nimport {ModelResponseComment} from \"../ModelResponseComment/ModelResponseComment.js\";\nimport {ModelMessageCopyButton} from \"./components/ModelMessageCopyButton/ModelMessageCopyButton.js\";\n\nimport \"./ModelMessage.css\";\n\nexport function ModelMessage({modelMessage, active}: ModelMessageProps) {\n return <div className=\"message model\">\n {\n modelMessage.message.map((message, responseIndex) => {\n const isLastMessage = responseIndex === modelMessage.message.length - 1;\n\n if (message.type === \"segment\") {\n if (message.segmentType === \"thought\")\n return <ModelResponseThought\n key={responseIndex}\n text={message.text}\n active={isLastMessage && active}\n duration={\n (message.startTime != null && message.endTime != null)\n ? (new Date(message.endTime).getTime() - new Date(message.startTime).getTime())\n : undefined\n }\n />;\n else if (message.segmentType === \"comment\")\n return <ModelResponseComment\n key={responseIndex}\n text={message.text}\n active={isLastMessage && active}\n />;\n else\n // ensure we handle all segment types or TypeScript will complain\n void (message.segmentType satisfies never);\n }\n\n return <MessageMarkdown\n key={responseIndex}\n activeDot={isLastMessage && active}\n className=\"text\"\n >\n {message.text}\n </MessageMarkdown>;\n })\n }\n {\n (modelMessage.message.length === 0 && active) &&\n <MessageMarkdown className=\"text\" activeDot />\n }\n <div className=\"buttons\" inert={active}>\n <ModelMessageCopyButton modelMessage={modelMessage.message} />\n </div>\n </div>;\n}\n\ntype ModelMessageProps = {\n modelMessage: SimplifiedModelChatItem,\n active: boolean\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","components","ModelMessageCopyButton","ModelMessageCopyButton.css"],"content":".appChatHistory > .message.model > .buttons {\n > .copyButton {\n display: grid;\n grid-template-areas: \"icon\";\n padding: 6px;\n border: none;\n\n transition: background-color 0.1s ease-in-out;\n\n &:not(:hover, :focus-visible) {\n background-color: transparent;\n }\n\n &.copied {\n > .icon.copy {\n opacity: 0;\n transition-delay: 0s;\n }\n\n > .icon.check {\n opacity: 1;\n transition-delay: 0.1s;\n }\n }\n\n > .icon {\n grid-area: icon;\n width: 18px;\n height: 18px;\n\n transition: opacity 0.3s ease-in-out;\n\n &.copy {\n opacity: 1;\n transition-delay: 0.1s;\n }\n &.check {\n opacity: 0;\n }\n }\n }\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","components","ModelMessageCopyButton","ModelMessageCopyButton.tsx"],"content":"import classNames from \"classnames\";\nimport {useCallback, useState} from \"react\";\nimport {CopyIconSVG} from \"../../../../../../../icons/CopyIconSVG.js\";\nimport {CheckIconSVG} from \"../../../../../../../icons/CheckIconSVG.js\";\nimport {SimplifiedModelChatItem} from \"../../../../../../../../electron/state/llmState.js\";\n\nimport \"./ModelMessageCopyButton.css\";\n\nconst showCopiedTime = 1000 * 2;\n\nexport function ModelMessageCopyButton({modelMessage}: ModelMessageCopyButtonProps) {\n const [copies, setCopies] = useState(0);\n\n const onClick = useCallback(() => {\n const text = modelMessage\n .filter((item) => item.type === \"text\")\n .map((item) => item.text)\n .join(\"\\n\")\n .trim();\n\n navigator.clipboard.writeText(text)\n .then(() => {\n setCopies(copies + 1);\n\n setTimeout(() => {\n setCopies(copies - 1);\n }, showCopiedTime);\n })\n .catch((error) => {\n console.error(\"Failed to copy text to clipboard\", error);\n });\n }, [modelMessage]);\n\n return <button\n onClick={onClick}\n className={classNames(\"copyButton\", copies > 0 && \"copied\")}\n >\n <CopyIconSVG className=\"icon copy\" />\n <CheckIconSVG className=\"icon check\" />\n </button>;\n}\n\ntype ModelMessageCopyButtonProps = {\n modelMessage: SimplifiedModelChatItem[\"message\"]\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseComment","ModelResponseComment.css"],"content":".appChatHistory > .message.model > .responseComment {\n padding: 4px 16px 0px 16px;\n position: relative;\n margin: 4px -8px;\n border-radius: 12px;\n\n transition: margin-bottom 0.3s var(--transition-easing), background-color 0.5s var(--transition-easing);\n\n &.active {\n margin-bottom: 8px;\n\n > .header > .opener > .summary {\n opacity: 1;\n\n > .title {\n opacity: 0.6;\n font-weight: bold;\n --generating-animation-mask-transparency-color: rgb(0 0 0 / 48%);\n\n animation-play-state: running;\n }\n\n > .chevron {\n opacity: 0.48;\n }\n }\n }\n\n &.open {\n margin-bottom: 20px;\n background-color: var(--model-comment-block-background-color);\n\n > .header > .opener > .summary > .chevron {\n transform: rotate(90deg);\n margin-inline-end: -2px;\n }\n }\n\n > .header {\n display: flex;\n flex-direction: row;\n\n > .opener {\n border: none;\n background-color: var(--model-comment-block-button-background-color);\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n margin: 0px 0px 0px -12px;\n border-radius: 12px;\n user-select: none;\n outline: solid 2px transparent;\n outline-offset: 4px;\n align-self: flex-start;\n max-width: 100%;\n opacity: 0.64;\n\n transition: opacity 0.3s var(--transition-easing);\n\n &:focus-visible {\n outline: solid 2px Highlight;\n outline-offset: 0px;\n }\n\n &:hover {\n opacity: 0.82;\n }\n\n > .summary {\n display: flex;\n flex-direction: row;\n align-items: center;\n\n > .title {\n --generating-animation-mask-transparency-color: rgb(0 0 0 / 100%);\n transition: font-weight 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing), --generating-animation-mask-transparency-color 0.3s var(--transition-easing), margin-bottom 0.3s var(--transition-easing);\n mask: linear-gradient(\n to right,\n var(--generating-animation-mask-transparency-color) 34%,\n black,\n var(--generating-animation-mask-transparency-color) 66%\n ) content-box 0 0 / 300% 100% no-repeat;\n animation: generating-animation 2s infinite ease-in-out;\n animation-play-state: paused;\n white-space: nowrap;\n }\n\n > .chevron {\n flex-shrink: 0;\n\n width: 20px;\n height: 20px;\n margin: -4px;\n margin-inline-start: 0px;\n margin-inline-end: -6px;\n opacity: 0.64;\n\n transform-origin: 56% 56%;\n transition: transform 0.2s var(--transition-easing), margin-inline-end 0.2s var(--transition-easing), opacity 0.3s var(--transition-easing);\n }\n }\n }\n\n > .excerpt {\n white-space: nowrap;\n overflow: hidden;\n display: flex;\n justify-content: end;\n align-self: center;\n width: calc-size(fit-content, min(360px, size + 8px));\n opacity: 0.24;\n mask: linear-gradient(to right, transparent, black 64px);\n margin-inline-start: 4px;\n\n user-select: none;\n\n interpolate-size: allow-keywords;\n transition: margin-inline-start 0.2s var(--transition-easing), width 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &.hide {\n width: calc-size(fit-content, max(0px, min(360px, size + 8px) - 24px));\n opacity: 0;\n margin-inline-start: 0px; /* this is to offset the chevron right margin on open */\n transition-delay: 0s, 0s;\n }\n }\n }\n\n > .comment {\n margin-top: 16px;\n padding-bottom: 12px;\n display: flex;\n flex-direction: column;\n interpolate-size: allow-keywords;\n transition: height 0.5s var(--transition-easing), margin-top 0.5s var(--transition-easing), padding-bottom 0.5s var(--transition-easing), margin-bottom 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &.hide {\n margin-top: 32px;\n height: 0px;\n margin-bottom: -12px;\n padding-bottom: 0px;\n opacity: 0;\n transition-delay: 0s, 0s, 0s, 0s;\n }\n\n > .content {\n opacity: 0.64;\n justify-self: flex-start;\n position: relative;\n overflow: hidden;\n max-height: 100%;\n }\n }\n}\n\n@keyframes generating-animation {\n 0% {\n mask-position: 100% 100%;\n }\n\n 100% {\n mask-position: 0 100%;\n }\n}\n\n@property --generating-animation-mask-transparency-color {\n syntax: \"<color>\";\n inherits: false;\n initial-value: transparent;\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseComment","ModelResponseComment.tsx"],"content":"import classNames from \"classnames\";\nimport {useCallback, useMemo, useState} from \"react\";\nimport {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {RightChevronIconSVG} from \"../../../../../icons/RightChevronIconSVG.js\";\nimport {MarkdownContent} from \"../../../MarkdownContent/MarkdownContent.js\";\n\nimport \"./ModelResponseComment.css\";\n\nconst excerptLength = 1024;\n\nexport function ModelResponseComment({text, active}: ModelResponseCommentProps) {\n const [isOpen, setIsOpen] = useState(false);\n\n const toggleIsOpen = useCallback(() => {\n setIsOpen((isOpen) => !isOpen);\n }, []);\n\n const title = useMemo(() => {\n if (active)\n return \"Generating comment\";\n\n return \"Generated comment\";\n }, [active]);\n\n return <div className={classNames(\"responseComment\", active && \"active\", isOpen && \"open\")}>\n <div className=\"header\">\n <button className=\"opener\" onClick={toggleIsOpen}>\n <span className=\"summary\">\n <div className=\"title\">{title}</div>\n <RightChevronIconSVG className=\"chevron\" />\n\n </span>\n </button>\n <MarkdownContent\n className={classNames(\"excerpt\", isOpen && \"hide\")}\n dir=\"auto\"\n inline\n >\n {text.slice(-excerptLength)}\n </MarkdownContent>\n </div>\n <div className={classNames(\"comment\", !isOpen && \"hide\")}>\n <MessageMarkdown className=\"content\" activeDot={active}>{text}</MessageMarkdown>\n </div>\n </div>;\n}\n\ntype ModelResponseCommentProps = {\n text: string,\n active: boolean\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseThought","ModelResponseThought.css"],"content":".appChatHistory > .message.model > .responseThought {\n padding: 0px 8px;\n\n transition: margin-bottom 0.3s var(--transition-easing);\n\n &.active {\n margin-bottom: 8px;\n\n > .header {\n > .summary {\n opacity: 1;\n\n > .title {\n opacity: 0.6;\n font-weight: bold;\n --thinking-animation-mask-transparency-color: rgb(0 0 0 / 48%);\n\n animation-play-state: running;\n }\n\n > .chevron {\n opacity: 0.48;\n }\n }\n }\n }\n\n &.open {\n > .header {\n > .summary {\n > .chevron {\n transform: rotate(90deg);\n }\n }\n }\n }\n\n > .header {\n border: none;\n background-color: transparent;\n display: flex;\n flex-direction: column;\n padding: 0px;\n user-select: none;\n outline: solid 2px transparent;\n border-radius: 4px;\n outline-offset: 4px;\n align-self: flex-start;\n max-width: 100%;\n\n &:focus-visible {\n outline: solid 2px Highlight;\n }\n\n &:hover > .summary {\n opacity: 1;\n }\n\n > .summary {\n display: flex;\n flex-direction: row;\n align-items: center;\n opacity: 0.64;\n\n transition: opacity 0.3s var(--transition-easing);\n\n > .title {\n --thinking-animation-mask-transparency-color: rgb(0 0 0 / 100%);\n transition: font-weight 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing), --thinking-animation-mask-transparency-color 0.3s var(--transition-easing), margin-bottom 0.3s var(--transition-easing);\n mask: linear-gradient(\n to right,\n var(--thinking-animation-mask-transparency-color) 34%,\n black,\n var(--thinking-animation-mask-transparency-color) 66%\n ) content-box 0 0 / 300% 100% no-repeat;\n animation: thinking-animation 2s infinite ease-in-out;\n animation-play-state: paused;\n }\n\n > .chevron {\n flex-shrink: 0;\n\n width: 20px;\n height: 20px;\n margin: -4px;\n margin-inline-start: 0px;\n opacity: 0.64;\n\n transform-origin: 56% 56%;\n transition: transform 0.2s var(--transition-easing), opacity 0.3s var(--transition-easing);\n }\n }\n\n > .excerpt {\n white-space: nowrap;\n overflow: hidden;\n display: flex;\n justify-content: end;\n justify-self: flex-start;\n mask: linear-gradient(to right, transparent, black 48px);\n max-width: calc-size(fit-content, min(360px, size + 8px));\n opacity: 0.24;\n font-size: 14px;\n margin-top: 2px;\n user-select: none;\n padding-inline-end: 24px;\n\n interpolate-size: allow-keywords;\n transition: height 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &.hide {\n height: 0px;\n opacity: 0;\n transition-delay: 0s, 0s;\n }\n }\n }\n\n > .content {\n margin-top: 16px;\n margin-bottom: 24px;\n opacity: 0.64;\n padding-left: 24px;\n justify-self: flex-start;\n position: relative;\n overflow: clip;\n\n interpolate-size: allow-keywords;\n transition: height 0.5s var(--transition-easing), margin-bottom 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &:before {\n content: \"\";\n position: absolute;\n width: 4px;\n height: 100%;\n background-color: var(--message-blockquote-border-color);\n left: 0px;\n }\n\n &.hide {\n height: 0px;\n margin-bottom: 0px;\n opacity: 0;\n transition-delay: 0s, 0s;\n }\n }\n}\n\n@keyframes thinking-animation {\n 0% {\n mask-position: 100% 100%;\n }\n\n 100% {\n mask-position: 0 100%;\n }\n}\n\n@property --thinking-animation-mask-transparency-color {\n syntax: \"<color>\";\n inherits: false;\n initial-value: transparent;\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseThought","ModelResponseThought.tsx"],"content":"import classNames from \"classnames\";\nimport {useCallback, useMemo, useState} from \"react\";\nimport prettyMilliseconds from \"pretty-ms\";\nimport {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {RightChevronIconSVG} from \"../../../../../icons/RightChevronIconSVG.js\";\nimport {MarkdownContent} from \"../../../MarkdownContent/MarkdownContent.js\";\n\nimport \"./ModelResponseThought.css\";\n\nconst excerptLength = 1024;\n\nexport function ModelResponseThought({text, active, duration}: ModelResponseThoughtProps) {\n const [isOpen, setIsOpen] = useState(false);\n\n const toggleIsOpen = useCallback(() => {\n setIsOpen((isOpen) => !isOpen);\n }, []);\n\n const title = useMemo(() => {\n if (active)\n return \"Thinking\";\n else if (duration != null) {\n const formattedDuration = prettyMilliseconds(duration, {\n secondsDecimalDigits: duration < 1000 * 10 ? 2 : 0,\n verbose: true\n });\n return `Thought for ${formattedDuration}`;\n }\n\n return \"Finished thinking\";\n }, [active, duration]);\n\n return <div className={classNames(\"responseThought\", active && \"active\", isOpen && \"open\")}>\n <button className=\"header\" onClick={toggleIsOpen}>\n <span className=\"summary\">\n <div className=\"title\">{title}</div>\n <RightChevronIconSVG className=\"chevron\" />\n </span>\n <MarkdownContent\n className={classNames(\"excerpt\", isOpen && \"hide\")}\n dir=\"auto\"\n inline\n >\n {text.slice(-excerptLength)}\n </MarkdownContent>\n </button>\n <MessageMarkdown className={classNames(\"content\", !isOpen && \"hide\")} activeDot={active}>{text}</MessageMarkdown>\n </div>;\n}\n\ntype ModelResponseThoughtProps = {\n text: string,\n active: boolean,\n duration?: number\n};\n"},{"path":["src","App","components","ChatHistory","components","UserMessage","UserMessage.css"],"content":".appChatHistory > .message.user {\n align-self: flex-end;\n background-color: var(--user-message-background-color);\n padding: 8px 12px;\n border-radius: 12px;\n margin-top: 0px;\n margin-bottom: 16px;\n margin-inline-start: 48px;\n margin-inline-end: 12px;\n color: var(--user-message-text-color);\n word-break: break-word;\n max-width: calc(100% - 48px - 12px);\n box-sizing: border-box;\n\n &:not(:first-child) {\n margin-top: 36px;\n }\n}\n"},{"path":["src","App","components","ChatHistory","components","UserMessage","UserMessage.tsx"],"content":"import {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {SimplifiedUserChatItem} from \"../../../../../../electron/state/llmState.js\";\n\nimport \"./UserMessage.css\";\n\nexport function UserMessage({message}: UserMessageProps) {\n return <MessageMarkdown className=\"message user\">\n {message.message}\n </MessageMarkdown>;\n}\n\ntype UserMessageProps = {\n message: SimplifiedUserChatItem\n};\n"},{"path":["src","App","components","FixedDivWithSpacer","FixedDivWithSpacer.tsx"],"content":"import React, {useLayoutEffect, useRef} from \"react\";\nimport classNames from \"classnames\";\n\nexport function FixedDivWithSpacer({className, ...props}: FixedDivWithSpacerProps) {\n const spacerRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n if (spacerRef.current == null)\n return;\n\n const spacerTag = spacerRef.current;\n const mainTag = spacerTag.previousElementSibling as HTMLDivElement | null;\n\n if (mainTag == null)\n return;\n\n const resizeObserver = new ResizeObserver(() => {\n spacerTag.style.width = `${mainTag.offsetWidth}px`;\n spacerTag.style.height = `${mainTag.offsetHeight}px`;\n });\n resizeObserver.observe(mainTag, {\n box: \"content-box\"\n });\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [spacerRef]);\n\n return <>\n <div className={classNames(className, \"main\")} {...props} />\n <div ref={spacerRef} className={classNames(className, \"spacer\")} />\n </>;\n}\n\ntype DivProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;\ntype FixedDivWithSpacerProps = DivProps;\n"},{"path":["src","App","components","Header","Header.css"],"content":".appHeader {\n display: flex;\n flex-direction: row;\n top: 16px;\n pointer-events: none;\n\n &.spacer {\n position: sticky;\n }\n\n &.main {\n width: calc(100% - 16px * 2);\n max-width: var(--app-max-width, 1280px);\n position: fixed;\n z-index: 10;\n\n > .panel {\n pointer-events: all;\n display: flex;\n flex-direction: row;\n align-self: start;\n background-color: var(--panel-background-color);\n border-radius: 12px;\n backdrop-filter: blur(8px);\n box-shadow: var(--panel-box-shadow);\n overflow: clip;\n isolation: isolate;\n color: var(--panel-text-color);\n z-index: 10;\n\n > button {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 8px 12px;\n margin: 8px;\n background-color: var(--panel-button-background-color);\n color: var(--panel-text-color);\n fill: var(--panel-text-color);\n\n + button {\n margin-inline-start: 0px;\n }\n\n &:hover,\n &:focus,\n &:focus-visible {\n border-color: var(--panel-button-hover-border-color);\n }\n\n > .icon {\n width: 20px;\n height: 20px;\n }\n }\n }\n\n > .model {\n position: relative;\n\n > .progress {\n position: absolute;\n inset-inline-start: 0;\n top: 0;\n bottom: 0;\n background-color: var(--panel-progress-color);\n width: calc(var(--progress) * 100%);\n pointer-events: none;\n z-index: -1;\n\n --progress: 0;\n\n &.hide {\n opacity: 0;\n\n transition: opacity 0.3s var(--transition-easing);\n }\n }\n\n > .modelName,\n > .noModel {\n flex: 1;\n text-align: start;\n align-self: center;\n flex-basis: 400px;\n padding: 12px 24px;\n word-break: break-word;\n\n margin-inline-end: 48px;\n }\n }\n\n > .spacer {\n flex-grow: 1;\n }\n }\n}\n"},{"path":["src","App","components","Header","Header.tsx"],"content":"import {CSSProperties} from \"react\";\nimport classNames from \"classnames\";\nimport {LoadFileIconSVG} from \"../../../icons/LoadFileIconSVG.tsx\";\nimport {DeleteIconSVG} from \"../../../icons/DeleteIconSVG.tsx\";\nimport {FixedDivWithSpacer} from \"../FixedDivWithSpacer/FixedDivWithSpacer.tsx\";\nimport {UpdateBadge} from \"./components/UpdateBadge.tsx\";\n\nimport \"./Header.css\";\n\n\nexport function Header({appVersion, canShowCurrentVersion, modelName, onLoadClick, loadPercentage, onResetChatClick}: HeaderProps) {\n // we use a FixedDivWithSpacer to push down the content while keeping the header fixed.\n // this allows the content to have macOS's scroll bounce while keeping the header fixed at the top.\n return <FixedDivWithSpacer className=\"appHeader\">\n <div className=\"panel model\">\n <div\n className={classNames(\"progress\", loadPercentage === 1 && \"hide\")}\n style={{\n \"--progress\": loadPercentage != null ? (loadPercentage * 100) : undefined\n } as CSSProperties}\n />\n\n {\n modelName != null &&\n <div className=\"modelName\">{modelName}</div>\n }\n {\n modelName == null &&\n <div className=\"noModel\">No model loaded</div>\n }\n\n <button\n className=\"resetChatButton\"\n disabled={onResetChatClick == null}\n onClick={onResetChatClick}\n >\n <DeleteIconSVG className=\"icon\" />\n </button>\n <button className=\"loadModelButton\" onClick={onLoadClick} disabled={onLoadClick == null}>\n <LoadFileIconSVG className=\"icon\" />\n </button>\n </div>\n <div className=\"spacer\" />\n <UpdateBadge\n appVersion={appVersion}\n canShowCurrentVersion={canShowCurrentVersion}\n />\n </FixedDivWithSpacer>;\n}\n\ntype HeaderProps = {\n appVersion?: string,\n canShowCurrentVersion?: boolean,\n modelName?: string,\n onLoadClick?(): void,\n loadPercentage?: number,\n onResetChatClick?(): void\n};\n"},{"path":["src","App","components","Header","components","UpdateBadge.css"],"content":".appHeader > .updateBadge {\n pointer-events: all;\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-shrink: 0;\n margin-inline-start: 16px;\n\n > .currentVersion {\n opacity: 0.4;\n margin: 14px;\n\n > code {\n background-color: transparent;\n }\n }\n\n > .newVersion {\n background-color: var(--update-badge-background-color);\n border: solid 1px var(--update-badge-border-color);\n box-shadow: var(--update-badge-box-shadow);\n border-radius: 12px;\n backdrop-filter: blur(8px);\n padding: 8px 12px;\n margin: 0px 8px;\n\n > .version {\n margin: 0px 4px;\n font-family: monospace;\n }\n }\n}\n"},{"path":["src","App","components","Header","components","UpdateBadge.tsx"],"content":"import {useCallback, useEffect, useMemo, useRef, useState} from \"react\";\nimport {withLock} from \"lifecycle-utils\";\nimport semver from \"semver\";\n\nimport \"./UpdateBadge.css\";\n\nconst latestReleaseUrl = \"https://github.com/withcatai/node-llama-cpp/releases/latest\";\nconst checkInterval = 1000 * 60 * 60 * 24;\n\n\nexport function UpdateBadge({appVersion, canShowCurrentVersion}: UpdateBadgeProps) {\n const [latestVersion, setLatestVersion] = useState<string | null>(null);\n const [releaseLink, setReleaseLink] = useState<string | null>(null);\n const shouldUpdateCurrentVersion = useRef(true);\n const nextUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const instanceLock = useRef({});\n\n const appVersionIsBeta = useMemo(() => {\n if (appVersion == null)\n return null;\n\n const componenets = semver.prerelease(appVersion);\n return componenets?.includes(\"beta\") ?? false;\n }, [appVersion]);\n\n const updateLatestVersionInfo = useCallback(async () => {\n clearTimeout(nextUpdateTimeoutRef.current);\n await withLock([instanceLock.current, \"updateVersion\"], async () => {\n clearTimeout(nextUpdateTimeoutRef.current);\n\n const latestVersion = await getLatestAvailableVersion(appVersionIsBeta ?? false);\n if (shouldUpdateCurrentVersion.current && latestVersion.version != null) {\n setLatestVersion(latestVersion.version);\n setReleaseLink(latestVersion.url);\n }\n\n nextUpdateTimeoutRef.current = setTimeout(updateLatestVersionInfo, checkInterval);\n });\n }, [appVersionIsBeta]);\n\n useEffect(() => {\n if (appVersionIsBeta == null)\n return;\n\n shouldUpdateCurrentVersion.current = true;\n void updateLatestVersionInfo();\n\n return () => {\n shouldUpdateCurrentVersion.current = false;\n clearTimeout(nextUpdateTimeoutRef.current);\n };\n }, [appVersionIsBeta]);\n\n const releasedVersionIsNewerThanCurrent = useMemo(() => {\n if (appVersion == null || latestVersion == null)\n return false;\n\n try {\n return semver.gt(latestVersion, appVersion);\n } catch (err) {\n return true;\n }\n }, [appVersion, latestVersion]);\n\n if (latestVersion == null)\n return null;\n\n return <div className=\"updateBadge\">\n {\n (!releasedVersionIsNewerThanCurrent && appVersion && canShowCurrentVersion) &&\n <div className=\"currentVersion\"><code>v{appVersion}</code></div>\n }\n {\n (releasedVersionIsNewerThanCurrent && releaseLink != null) &&\n <a\n target=\"_blank\"\n href={releaseLink}\n className=\"newVersion\"\n >\n Version <code className=\"version\">{latestVersion}</code> is available\n </a>\n }\n </div>;\n}\n\ntype UpdateBadgeProps = {\n appVersion?: string,\n canShowCurrentVersion?: boolean\n};\n\nasync function getLatestAvailableVersion(includePrerelease: boolean = false): Promise<{\n version?: string,\n url: string\n}> {\n try {\n if (includePrerelease) {\n const latestReleases = await getLatestPrereleaseAndRelease();\n if (latestReleases.latestPrerelease != null && latestReleases.latestRelease != null) {\n if (semver.gt(latestReleases.latestPrerelease.version, latestReleases.latestRelease.version))\n return {\n version: latestReleases.latestPrerelease.version,\n url: latestReleases.latestPrerelease.url\n };\n\n return {\n version: latestReleases.latestRelease.version,\n url: latestReleaseUrl\n };\n } else if (latestReleases.latestPrerelease != null) {\n return {\n version: latestReleases.latestPrerelease.version,\n url: latestReleases.latestPrerelease.url\n };\n } else if (latestReleases.latestRelease != null) {\n return {\n version: latestReleases.latestRelease.version,\n url: latestReleaseUrl\n };\n }\n }\n\n const releaseRes = await fetch(\"https://api.github.com/repos/withcatai/node-llama-cpp/releases/latest\");\n const release: {\n tag_name: string\n } = await releaseRes.json();\n\n return {\n version: normalizeTagName(release?.tag_name),\n url: latestReleaseUrl\n };\n } catch (err) {\n console.error(err);\n return {\n version: undefined,\n url: latestReleaseUrl\n };\n }\n}\n\nasync function getLatestPrereleaseAndRelease(): Promise<{\n latestRelease?: {\n version: string,\n url: string\n },\n latestPrerelease?: {\n version: string,\n url: string\n }\n}> {\n try {\n const releasesRes = await fetch(\"https://api.github.com/repos/withcatai/node-llama-cpp/releases?per_page=100\");\n const releases: Array<{\n tag_name: string,\n html_url: string,\n prerelease: boolean,\n draft: boolean\n }> = await releasesRes.json();\n\n const latestRelease = releases.find((release) => !release.prerelease && !release.draft);\n const latestPrerelease = releases.find((release) => release.prerelease && !release.draft);\n\n return {\n latestRelease: latestRelease == null ? undefined : {\n version: normalizeTagName(latestRelease.tag_name)!,\n url: latestRelease.html_url\n },\n latestPrerelease: latestPrerelease == null ? undefined : {\n version: normalizeTagName(latestPrerelease.tag_name)!,\n url: latestPrerelease.html_url\n }\n };\n } catch (err) {\n console.error(err);\n return {};\n }\n}\n\nfunction normalizeTagName(tagName?: string) {\n if (tagName == null)\n return undefined;\n\n if (tagName.toLowerCase().startsWith(\"v\"))\n return tagName.slice(\"v\".length);\n\n return tagName;\n}\n"},{"path":["src","App","components","InputRow","InputRow.css"],"content":".appInputRow {\n display: flex;\n flex-direction: row;\n bottom: 16px;\n flex-shrink: 0;\n align-items: flex-end;\n\n &.spacer {\n position: sticky;\n pointer-events: none;\n }\n\n &.main {\n width: calc(100% - 16px * 2);\n max-width: var(--app-max-width, 1280px);\n position: fixed;\n background-color: var(--panel-background-color);\n border-radius: 12px;\n backdrop-filter: blur(8px);\n box-shadow: var(--panel-box-shadow);\n overflow: clip;\n color: var(--panel-text-color);\n z-index: 10;\n\n &.disabled {\n opacity: 0.48;\n }\n\n > .inputContainer {\n flex: 1;\n display: flex;\n flex-direction: row;\n overflow: hidden;\n position: relative;\n isolation: isolate;\n max-height: 400px;\n min-height: var(--min-height);\n --min-height: 55px;\n\n > .input {\n flex: 1;\n border: none;\n resize: none;\n box-sizing: border-box;\n max-height: 160px;\n min-height: var(--min-height);\n height: 55px;\n outline: none;\n padding: calc((var(--min-height) - 1lh) / 2) 24px;\n background-color: transparent;\n font: inherit;\n align-content: center;\n align-self: stretch;\n color: var(--panel-text-color);\n z-index: 2;\n unicode-bidi: plaintext;\n overflow: auto;\n\n &::placeholder {\n color: var(--panel-text-color);\n opacity: 0.4;\n }\n }\n\n > .autocomplete {\n position: absolute;\n inset: 0px;\n z-index: 1;\n display: flex;\n overflow: hidden;\n pointer-events: none;\n user-select: none;\n\n > .content {\n flex: 1;\n flex-shrink: 0;\n font: inherit;\n padding: calc((var(--min-height) - 1lh) / 2) 24px;\n text-align: initial;\n unicode-bidi: plaintext;\n overflow: hidden;\n opacity: 0.36;\n mask: linear-gradient(to top, rgb(0 0 0 / 16%), black 24px);\n\n &.hide {\n opacity: 0;\n }\n\n > .currentText {\n opacity: 0;\n display: inline;\n white-space: pre-wrap;\n word-break: break-word;\n unicode-bidi: normal;\n }\n\n > .completion {\n display: inline;\n white-space: pre-wrap;\n word-break: break-word;\n unicode-bidi: normal;\n }\n\n > .pressTab {\n display: inline-block;\n margin: -1px 8px;\n opacity: 0.8;\n border: solid 1px color-mix(in srgb, currentColor, transparent 64%);\n border-bottom-width: 2px;\n border-radius: 8px;\n padding: 0.1em 0.4em;\n font-size: 0.8em;\n vertical-align: top;\n }\n }\n }\n }\n\n > .stopGenerationButton,\n > .sendButton {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 8px 12px;\n margin: 8px;\n background-color: var(--panel-button-background-color);\n color: var(--panel-text-color);\n fill: var(--panel-text-color);\n\n + button {\n margin-inline-start: 0px;\n }\n\n &:hover,\n &:focus,\n &:focus-visible {\n border-color: var(--panel-button-hover-border-color);\n }\n\n > .icon {\n width: 20px;\n height: 20px;\n }\n }\n\n > .stopGenerationButton {\n transition: border-color 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing);\n\n &[disabled] {\n opacity: 0;\n }\n }\n }\n}\n"},{"path":["src","App","components","InputRow","InputRow.tsx"],"content":"import {useCallback, useMemo, useRef, useState} from \"react\";\nimport classNames from \"classnames\";\nimport {AddMessageIconSVG} from \"../../../icons/AddMessageIconSVG.tsx\";\nimport {AbortIconSVG} from \"../../../icons/AbortIconSVG.tsx\";\nimport {FixedDivWithSpacer} from \"../FixedDivWithSpacer/FixedDivWithSpacer.tsx\";\n\nimport \"./InputRow.css\";\n\n\nexport function InputRow({\n disabled = false, stopGeneration, sendPrompt, onPromptInput, autocompleteInputDraft, autocompleteCompletion, generatingResult\n}: InputRowProps) {\n const [inputText, setInputText] = useState<string>(\"\");\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const autocompleteRef = useRef<HTMLDivElement>(null);\n const autocompleteCurrentTextRef = useRef<HTMLDivElement>(null);\n\n const autocompleteText = useMemo(() => {\n const fullText = (autocompleteInputDraft ?? \"\") + (autocompleteCompletion ?? \"\");\n if (fullText.startsWith(inputText))\n return fullText.slice(inputText.length);\n\n return \"\";\n }, [inputText, autocompleteInputDraft, autocompleteCompletion]);\n\n const setInputValue = useCallback((value: string) => {\n if (inputRef.current != null)\n inputRef.current.value = value;\n\n if (autocompleteCurrentTextRef.current != null)\n autocompleteCurrentTextRef.current.innerText = value;\n\n setInputText(value);\n }, []);\n\n const resizeInput = useCallback(() => {\n if (inputRef.current == null)\n return;\n\n inputRef.current.style.height = \"\";\n inputRef.current.style.height = inputRef.current.scrollHeight + \"px\";\n\n if (autocompleteRef.current != null) {\n autocompleteRef.current.scrollTop = inputRef.current.scrollTop;\n }\n }, []);\n\n const submitPrompt = useCallback(() => {\n if (generatingResult || inputRef.current == null)\n return;\n\n const message = inputRef.current.value;\n if (message.length === 0)\n return;\n\n setInputValue(\"\");\n resizeInput();\n onPromptInput?.(\"\");\n sendPrompt(message);\n }, [setInputValue, generatingResult, resizeInput, sendPrompt, onPromptInput]);\n\n const onInput = useCallback(() => {\n setInputText(inputRef.current?.value ?? \"\");\n resizeInput();\n\n if (autocompleteCurrentTextRef.current != null && inputRef.current != null)\n autocompleteCurrentTextRef.current.innerText = inputRef.current?.value;\n\n if (inputRef.current != null && onPromptInput != null)\n onPromptInput(inputRef.current?.value);\n }, [resizeInput, onPromptInput]);\n\n const onInputKeyDown = useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n submitPrompt();\n } else if (event.key === \"Tab\" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {\n event.preventDefault();\n if (inputRef.current != null && autocompleteText !== \"\") {\n const newlineIndex = autocompleteText.indexOf(\"\\n\");\n const textToAccept = newlineIndex <= 0\n ? autocompleteText\n : autocompleteText.slice(0, newlineIndex);\n\n setInputValue(inputRef.current.value + textToAccept);\n inputRef.current.scrollTop = inputRef.current.scrollHeight;\n onPromptInput?.(inputRef.current.value);\n }\n\n resizeInput();\n }\n }, [submitPrompt, setInputValue, onPromptInput, resizeInput, autocompleteText]);\n\n const previewAutocompleteText = useMemo(() => {\n const lines = autocompleteText.split(\"\\n\");\n if (lines.length <= 1 || lines[1]!.trim() === \"\")\n return lines[0]!;\n\n return autocompleteText;\n }, [autocompleteText]);\n\n // we use a FixedDivWithSpacer to push down the content while keeping the input fixed.\n // this allows the content to have macOS's scroll bounce while keeping the input fixed at the bottom.\n return <FixedDivWithSpacer className={classNames(\"appInputRow\", disabled && \"disabled\")}>\n <div className=\"inputContainer\">\n <textarea\n ref={inputRef}\n onInput={onInput}\n onKeyDownCapture={onInputKeyDown}\n className=\"input\"\n autoComplete=\"off\"\n spellCheck\n disabled={disabled}\n onScroll={resizeInput}\n placeholder={\n autocompleteText === \"\"\n ? \"Type a message...\"\n : \"\"\n }\n />\n <div className=\"autocomplete\" ref={autocompleteRef}>\n <div className={classNames(\"content\", autocompleteText === \"\" && \"hide\")}>\n <div className=\"currentText\" ref={autocompleteCurrentTextRef} />\n <div className=\"completion\">{previewAutocompleteText}</div>\n <div className=\"pressTab\">Tab</div>\n </div>\n </div>\n </div>\n <button\n className=\"stopGenerationButton\"\n disabled={disabled || stopGeneration == null || !generatingResult}\n onClick={stopGeneration}\n >\n <AbortIconSVG className=\"icon\" />\n </button>\n <button\n className=\"sendButton\"\n disabled={disabled || inputText === \"\" || generatingResult}\n onClick={submitPrompt}\n >\n <AddMessageIconSVG className=\"icon\" />\n </button>\n </FixedDivWithSpacer>;\n}\n\ntype InputRowProps = {\n disabled?: boolean,\n stopGeneration?(): void,\n sendPrompt(prompt: string): void,\n onPromptInput?(currentText: string): void,\n autocompleteInputDraft?: string,\n autocompleteCompletion?: string,\n generatingResult: boolean\n};\n"},{"path":["src","App","components","MarkdownContent","MarkdownContent.css"],"content":"/* Only style code blocks */\npre > code {\n display: block;\n background-color: var(--code-block-background-color);\n padding: 0.8em 1.2em;\n border-radius: 0.4em;\n font-size: 1.2em;\n overflow-x: auto;\n user-select: all;\n\n @media (prefers-color-scheme: light) {\n /*!\n Theme: GitHub\n Description: Light theme as seen on github.com\n Author: github.com\n Maintainer: @Hirse\n Updated: 2021-05-15\n\n Outdated base version: https://github.com/primer/github-syntax-light\n Current colors taken from GitHub's CSS\n */\n .hljs {\n color: #24292e;\n background: #ffffff\n }\n .hljs-doctag,\n .hljs-keyword,\n .hljs-meta .hljs-keyword,\n .hljs-template-tag,\n .hljs-template-variable,\n .hljs-type,\n .hljs-variable.language_ {\n /* prettylights-syntax-keyword */\n color: #d73a49\n }\n .hljs-title,\n .hljs-title.class_,\n .hljs-title.class_.inherited__,\n .hljs-title.function_ {\n /* prettylights-syntax-entity */\n color: #6f42c1\n }\n .hljs-attr,\n .hljs-attribute,\n .hljs-literal,\n .hljs-meta,\n .hljs-number,\n .hljs-operator,\n .hljs-variable,\n .hljs-selector-attr,\n .hljs-selector-class,\n .hljs-selector-id {\n /* prettylights-syntax-constant */\n color: #005cc5\n }\n .hljs-regexp,\n .hljs-string,\n .hljs-meta .hljs-string {\n /* prettylights-syntax-string */\n color: #032f62\n }\n .hljs-built_in,\n .hljs-symbol {\n /* prettylights-syntax-variable */\n color: #e36209\n }\n .hljs-comment,\n .hljs-code,\n .hljs-formula {\n /* prettylights-syntax-comment */\n color: #6a737d\n }\n .hljs-name,\n .hljs-quote,\n .hljs-selector-tag,\n .hljs-selector-pseudo {\n /* prettylights-syntax-entity-tag */\n color: #22863a\n }\n .hljs-subst {\n /* prettylights-syntax-storage-modifier-import */\n color: #24292e\n }\n .hljs-section {\n /* prettylights-syntax-markup-heading */\n color: #005cc5;\n font-weight: bold\n }\n .hljs-bullet {\n /* prettylights-syntax-markup-list */\n color: #735c0f\n }\n .hljs-emphasis {\n /* prettylights-syntax-markup-italic */\n color: #24292e;\n font-style: italic\n }\n .hljs-strong {\n /* prettylights-syntax-markup-bold */\n color: #24292e;\n font-weight: bold\n }\n .hljs-addition {\n /* prettylights-syntax-markup-inserted */\n color: #22863a;\n background-color: #f0fff4\n }\n .hljs-deletion {\n /* prettylights-syntax-markup-deleted */\n color: #b31d28;\n background-color: #ffeef0\n }\n .hljs-char.escape_,\n .hljs-link,\n .hljs-params,\n .hljs-property,\n .hljs-punctuation,\n .hljs-tag {\n /* purposely ignored */\n }\n }\n\n @media (prefers-color-scheme: dark) {\n /*!\n Theme: GitHub Dark\n Description: Dark theme as seen on github.com\n Author: github.com\n Maintainer: @Hirse\n Updated: 2021-05-15\n\n Outdated base version: https://github.com/primer/github-syntax-dark\n Current colors taken from GitHub's CSS\n */\n .hljs {\n color: #c9d1d9;\n background: #0d1117\n }\n .hljs-doctag,\n .hljs-keyword,\n .hljs-meta .hljs-keyword,\n .hljs-template-tag,\n .hljs-template-variable,\n .hljs-type,\n .hljs-variable.language_ {\n /* prettylights-syntax-keyword */\n color: #ff7b72\n }\n .hljs-title,\n .hljs-title.class_,\n .hljs-title.class_.inherited__,\n .hljs-title.function_ {\n /* prettylights-syntax-entity */\n color: #d2a8ff\n }\n .hljs-attr,\n .hljs-attribute,\n .hljs-literal,\n .hljs-meta,\n .hljs-number,\n .hljs-operator,\n .hljs-variable,\n .hljs-selector-attr,\n .hljs-selector-class,\n .hljs-selector-id {\n /* prettylights-syntax-constant */\n color: #79c0ff\n }\n .hljs-regexp,\n .hljs-string,\n .hljs-meta .hljs-string {\n /* prettylights-syntax-string */\n color: #a5d6ff\n }\n .hljs-built_in,\n .hljs-symbol {\n /* prettylights-syntax-variable */\n color: #ffa657\n }\n .hljs-comment,\n .hljs-code,\n .hljs-formula {\n /* prettylights-syntax-comment */\n color: #8b949e\n }\n .hljs-name,\n .hljs-quote,\n .hljs-selector-tag,\n .hljs-selector-pseudo {\n /* prettylights-syntax-entity-tag */\n color: #7ee787\n }\n .hljs-subst {\n /* prettylights-syntax-storage-modifier-import */\n color: #c9d1d9\n }\n .hljs-section {\n /* prettylights-syntax-markup-heading */\n color: #1f6feb;\n font-weight: bold\n }\n .hljs-bullet {\n /* prettylights-syntax-markup-list */\n color: #f2cc60\n }\n .hljs-emphasis {\n /* prettylights-syntax-markup-italic */\n color: #c9d1d9;\n font-style: italic\n }\n .hljs-strong {\n /* prettylights-syntax-markup-bold */\n color: #c9d1d9;\n font-weight: bold\n }\n .hljs-addition {\n /* prettylights-syntax-markup-inserted */\n color: #aff5b4;\n background-color: #033a16\n }\n .hljs-deletion {\n /* prettylights-syntax-markup-deleted */\n color: #ffdcd7;\n background-color: #67060c\n }\n .hljs-char.escape_,\n .hljs-link,\n .hljs-params,\n .hljs-property,\n .hljs-punctuation,\n .hljs-tag {\n /* purposely ignored */\n }\n }\n}\n"},{"path":["src","App","components","MarkdownContent","MarkdownContent.tsx"],"content":"import {useLayoutEffect, useRef} from \"react\";\nimport markdownit from \"markdown-it\";\nimport hljs from \"highlight.js\";\n\nimport \"./MarkdownContent.css\";\n\nconst md = markdownit({\n highlight(str, lang): string {\n if (hljs.getLanguage(lang) != null) {\n try {\n return hljs.highlight(str, {language: lang}).value;\n } catch (err) {\n // do nothing\n }\n }\n\n return hljs.highlightAuto(str).value;\n }\n});\n\nexport function MarkdownContent({children, inline = false, dir, className}: MarkdownContentProps) {\n const divRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n if (divRef.current == null)\n return;\n\n if (inline)\n divRef.current.innerHTML = md.renderInline(children ?? \"\").replaceAll(\"<br>\", \"\");\n else\n divRef.current.innerHTML = md.render(children ?? \"\");\n }, [inline, children]);\n\n return <div\n className={className}\n ref={divRef}\n dir={dir}\n />;\n}\n\ntype MarkdownContentProps = {\n className?: string,\n inline?: boolean,\n dir?: string,\n children: string\n};\n"},{"path":["src","App","components","MessageMarkdown","MessageMarkdown.css"],"content":".appMessageMarkdown {\n word-break: break-word;\n\n &.active {\n &:empty:after,\n &:not(:empty)>:last-child:not(ol, ul, table):after,\n &:not(:empty)>:last-child:where(ol, ul)>:last-child:not(:has(>:last-child:where(ol, ul))):after,\n &:not(:empty)>:last-child:where(ol, ul)>:last-child>:last-child:where(ol, ul)>:last-child:after,\n &:not(:empty)>:last-child:where(table)>:last-child>:last-child>:last-child:after {\n content: \"\";\n position: static;\n display: inline-block;\n background-color: currentColor;\n width: 8px;\n height: 8px;\n translate: 0px -2px;\n border-radius: 9999px;\n margin-inline-start: 8px;\n vertical-align: middle;\n\n animation: messageMarkdownActiveDot 2s infinite ease-in-out;\n }\n }\n\n > :first-child {\n margin-top: 0px;\n }\n\n > :last-child {\n margin-bottom: 0px;\n }\n\n * {\n unicode-bidi: plaintext;\n }\n\n h2 {\n margin: 16px 0px;\n padding-top: 24px;\n }\n\n h3 {\n margin: 32px 0px 0px 0px;\n }\n\n hr {\n background-color: var(--message-hr-color);\n height: 2px;\n border: none;\n border-radius: 12px;\n }\n\n blockquote {\n margin: 0px 0px;\n padding-inline-start: 24px;\n opacity: 0.64;\n border: none;\n position: relative;\n\n &:before {\n content: \"\";\n position: absolute;\n width: 4px;\n height: 100%;\n background-color: var(--message-blockquote-border-color);\n inset-inline-start: 0px;\n }\n }\n\n table {\n display: block;\n border-style: hidden;\n border-radius: 12px;\n outline: solid 1px var(--message-table-outline-color);\n outline-offset: -1px;\n max-width: max-content;\n border-collapse: collapse;\n overflow-x: auto;\n background-color: var(--background-color);\n\n thead {\n text-align: justify;\n }\n\n tr {\n background-color: var(--message-table-background-color);\n border-top: 1px solid var(--message-table-outline-color);\n\n &:nth-child(2n) td {\n background-color: var(--message-table-even-background-color);\n }\n\n th {\n background-color: var(--message-table-even-background-color);\n border: 1px solid var(--message-table-outline-color);\n padding: 8px 16px;\n }\n\n td {\n border: 1px solid var(--message-table-outline-color);\n padding: 8px 16px;\n }\n }\n }\n}\n\n@keyframes messageMarkdownActiveDot {\n 0% {\n transform: scale(1);\n opacity: 0.64;\n }\n 50% {\n transform: scale(1.4);\n opacity: 0.32;\n }\n 100% {\n transform: scale(1);\n opacity: 0.64;\n }\n}\n"},{"path":["src","App","components","MessageMarkdown","MessageMarkdown.tsx"],"content":"import {useMemo} from \"react\";\nimport classNames from \"classnames\";\nimport {MarkdownContent} from \"../MarkdownContent/MarkdownContent.js\";\n\nimport \"./MessageMarkdown.css\";\n\n\nexport function MessageMarkdown({children, activeDot = false, className}: MessageMarkdownProps) {\n const renderContent = useMemo(() => {\n if (children == null)\n return \"\";\n\n if (!activeDot)\n return children;\n\n const lines = children.split(\"\\n\");\n const lastLine = lines.at(-1);\n\n // to frequent line jumps and instability while the content is being generated,\n // wait with rendering the last line until its content is properly formed and is ready to be appended\n if (lastLine != null && [\"-\", \"+\", \"*\", \"1.\", \"1\", \"--\"].includes(lastLine.trim()))\n return lines.slice(0, -1).join(\"\\n\");\n else if (lastLine != null && lastLine.trim().length === 1 && (\n lastLine.endsWith(\" *\") || lastLine.endsWith(\" _\") || lastLine.endsWith(\" ~\")\n ))\n return [\n ...lines.slice(0, -1),\n lastLine.slice(0, -\" _\".length)\n ].join(\"\\n\");\n\n return children;\n }, [children, activeDot]);\n\n return <MarkdownContent className={classNames(\"appMessageMarkdown\", activeDot && \"active\", className)}>\n {renderContent}\n </MarkdownContent>;\n}\n\ntype MessageMarkdownProps = {\n children?: string,\n activeDot?: boolean,\n className?: string\n};\n"},{"path":["src","hooks","useExternalState.ts"],"content":"import {useEffect, useState} from \"react\";\nimport {State} from \"lifecycle-utils\";\n\nexport function useExternalState<const StateType, const R>(state: State<StateType>, selector: ((state: StateType) => R)): R;\nexport function useExternalState<const StateType>(state: State<StateType>): StateType;\nexport function useExternalState<const StateType>(state: State<StateType>, selector?: ((state: StateType) => any) | null): StateType {\n const [currentState, setCurrentState] = useState(() => (\n selector == null\n ? state.state\n : selector(state.state)\n ));\n\n useEffect(() => {\n return state.createChangeListener((newState) => {\n setCurrentState(\n selector == null\n ? newState\n : selector(newState)\n );\n }, true).dispose;\n }, [state]);\n\n return currentState;\n}\n"},{"path":["src","icons","AbortIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function AbortIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M360-320h240q17 0 28.5-11.5T640-360v-240q0-17-11.5-28.5T600-640H360q-17 0-28.5 11.5T320-600v240q0 17 11.5 28.5T360-320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z\" />\n </svg>;\n}\n"},{"path":["src","icons","AddMessageIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function AddMessageIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M440-647 244-451q-12 12-28 11.5T188-452q-11-12-11.5-28t11.5-28l264-264q6-6 13-8.5t15-2.5q8 0 15 2.5t13 8.5l264 264q11 11 11 27.5T772-452q-12 12-28.5 12T715-452L520-647v447q0 17-11.5 28.5T480-160q-17 0-28.5-11.5T440-200v-447Z\" />\n </svg>;\n}\n"},{"path":["src","icons","CheckIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function CheckIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z\" />\n </svg>;\n}\n"},{"path":["src","icons","CopyIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function CopyIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z\" />\n </svg>;\n}\n"},{"path":["src","icons","DeleteIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function DeleteIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M280-120q-33 0-56.5-23.5T200-200v-520q-17 0-28.5-11.5T160-760q0-17 11.5-28.5T200-800h160q0-17 11.5-28.5T400-840h160q17 0 28.5 11.5T600-800h160q17 0 28.5 11.5T800-760q0 17-11.5 28.5T760-720v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520Zm-400 0v520-520Zm200 316 76 76q11 11 28 11t28-11q11-11 11-28t-11-28l-76-76 76-76q11-11 11-28t-11-28q-11-11-28-11t-28 11l-76 76-76-76q-11-11-28-11t-28 11q-11 11-11 28t11 28l76 76-76 76q-11 11-11 28t11 28q11 11 28 11t28-11l76-76Z\" />\n </svg>;\n}\n"},{"path":["src","icons","DownloadIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function DownloadIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z\" />\n </svg>;\n}\n"},{"path":["src","icons","LoadFileIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function LoadFileIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M440-367v127q0 17 11.5 28.5T480-200q17 0 28.5-11.5T520-240v-127l36 36q6 6 13.5 9t15 2.5q7.5-.5 14.5-3.5t13-9q11-12 11.5-28T612-388L508-492q-6-6-13-8.5t-15-2.5q-8 0-15 2.5t-13 8.5L348-388q-12 12-11.5 28t12.5 28q12 11 28 11.5t28-11.5l35-35ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h287q16 0 30.5 6t25.5 17l194 194q11 11 17 25.5t6 30.5v447q0 33-23.5 56.5T720-80H240Zm280-560v-160H240v640h480v-440H560q-17 0-28.5-11.5T520-640ZM240-800v200-200 640-640Z\" />\n </svg>;\n}\n"},{"path":["src","icons","RightChevronIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function RightChevronIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M522-480 358-644q-11-11-11-25.5t11-25.5q11-11 25.5-11t25.84 11.34L599-505q5 5.4 7.5 11.7 2.5 6.3 2.5 13.5t-2.5 13.5Q604-460 599-455L409.34-265.34Q398-254 384-254.5T359-266q-11-11-11-25.5t11-25.5l163-163Z\" />\n </svg>;\n}\n"},{"path":["src","icons","SearchIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function SearchIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M380-320q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\" />\n </svg>;\n}\n"},{"path":["src","icons","StarIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function StarIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143Zm126 18L314-169q-11 7-23 6t-21-8q-9-7-14-17.5t-2-23.5l44-189-147-127q-10-9-12.5-20.5T140-571q4-11 12-18t22-9l194-17 75-178q5-12 15.5-18t21.5-6q11 0 21.5 6t15.5 18l75 178 194 17q14 2 22 9t12 18q4 11 1.5 22.5T809-528L662-401l44 189q3 13-2 23.5T690-171q-9 7-21 8t-23-6L480-269Zm0-201Z\" />\n </svg>;\n}\n"},{"path":["src","index.css"],"content":":root {\n font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n line-height: 1.5;\n font-weight: 400;\n\n color-scheme: light dark;\n background-color: var(--background-color);\n color: var(--text-color);\n\n font-synthesis: none;\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n\n --transition-easing: cubic-bezier(0.4, 0.0, 0.2, 1);\n}\n\n:root {\n --background-color: light-dark(#eeeeee, #242424);\n --text-color: light-dark(#242424, #eeeeee);\n\n --button-background-color: light-dark(rgba(255 255 255 / 64%), rgba(255 255 255 / 8%));\n --button-hover-border-color: light-dark(#646cff, #646cff);\n\n --link-color: light-dark(#646cff, #646cff);\n --link-hover-color: light-dark(#747bff, #535bf2);\n --star-link-color: light-dark(#e09c1c, #eac54f);\n --star-hover-color: light-dark(#daaa52, #c7a027);\n\n --panel-background-color: light-dark(rgba(0 0 0 / 52%), rgba(0 0 0 / 48%));\n --panel-text-color: light-dark(#ffffff, #eeeeee);\n --panel-button-background-color: light-dark(rgba(0 0 0 / 24%), rgba(255 255 255 / 8%));\n --panel-button-hover-border-color: light-dark(var(--panel-text-color), var(--button-hover-border-color));\n --panel-progress-color: light-dark(rgba(255 255 255 / 8%), rgba(255 255 255 / 4%));\n --panel-box-shadow: 0px 4px 8px 0px rgba(0 0 0 / 8%), 0px 6px 24px 0px rgba(0 0 0 / 16%);\n\n --error-border-color: light-dark(rgba(239 83 80 / 100%), rgba(239 83 80 / 100%));\n\n --user-message-background-color: light-dark(rgba(0 0 0 / 16%), rgba(0 0 0 / 48%));\n --user-message-text-color: light-dark(#242424, var(--text-color));\n\n --actions-block-background-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --actions-block-border-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --actions-block-box-shadow: 0px 4px 8px 0px rgba(0 0 0 / 2%), 0px 6px 24px 0px rgba(0 0 0 / 8%);\n\n --code-block-background-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n\n --update-badge-background-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --update-badge-border-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --update-badge-box-shadow: 0px 4px 8px 0px rgba(0 0 0 / 2%), 0px 6px 24px 0px rgba(0 0 0 / 8%);\n\n --message-table-outline-color: light-dark(rgba(0 0 0 / 8%), rgba(255 255 255 / 6%));\n --message-table-background-color: light-dark(rgba(0 0 0 / 0%), rgba(0 0 0 / 12%));\n --message-table-even-background-color: light-dark(rgba(0 0 0 / 4%), rgba(255 255 255 / 6%));\n\n --message-hr-color: light-dark(rgba(0 0 0 / 16%), rgba(255 255 255 / 16%));\n --message-blockquote-border-color: light-dark(rgba(0 0 0 / 8%), rgba(255 255 255 / 8%));\n\n --model-comment-block-background-color: light-dark(rgba(0 0 0 / 6%), rgba(0 0 0 / 24%));\n --model-comment-block-button-background-color: light-dark(rgba(0 0 0 / 6%), rgba(0 0 0 / 24%));\n}\n\nbody {\n margin: 0;\n display: flex;\n min-width: 320px;\n min-height: 100vh;\n overscroll-behavior-x: none;\n}\n\na {\n font-weight: 500;\n color: var(--link-color);\n text-decoration: inherit;\n\n &:hover {\n color: var(--link-hover-color);\n }\n}\n\ncode {\n background-color: color-mix(in srgb, currentColor, transparent 84%);\n padding: 0.1em 0.3em;\n border-radius: 0.4em;\n font-size: 1.2em;\n}\n\nbutton {\n border-radius: 8px;\n border: 1px solid transparent;\n padding: 0.6em 1.2em;\n font-size: 1em;\n font-weight: 500;\n font-family: inherit;\n background-color: var(--button-background-color);\n cursor: pointer;\n transition: border-color 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing);\n color: var(--text-color);\n fill: var(--text-color);\n\n &[disabled] {\n opacity: 0.52;\n pointer-events: none;\n }\n\n &:hover,\n &:focus,\n &:focus-visible {\n border-color: var(--button-hover-border-color);\n }\n}\n\n"},{"path":["src","index.html"],"content":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\"/>\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\"/>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n <title>Electron + TypeScript + React + Vite + node-llama-cpp</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"./index.tsx\"></script>\n </body>\n</html>\n"},{"path":["src","index.tsx"],"content":"import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport \"@fontsource-variable/inter/opsz-italic.css\";\nimport {App} from \"./App/App.tsx\";\nimport \"./index.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>\n);\n\n// Use contextBridge\nwindow.ipcRenderer.on(\"main-process-message\", (_event, message) => {\n console.log(message);\n});\n"},{"path":["src","rpc","llmRpc.ts"],"content":"import {ElectronFunctions} from \"../../electron/rpc/llmRpc.ts\";\nimport {createRendererSideBirpc} from \"../utils/createRendererSideBirpc.ts\";\nimport {llmState} from \"../state/llmState.ts\";\nimport {LlmState} from \"../../electron/state/llmState.ts\";\n\n\nconst renderedFunctions = {\n updateState(state: LlmState) {\n llmState.state = state;\n }\n} as const;\nexport type RenderedFunctions = typeof renderedFunctions;\n\nexport const electronLlmRpc = createRendererSideBirpc<ElectronFunctions, RenderedFunctions>(\"llmRpc\", \"llmRpc\", renderedFunctions);\n\nelectronLlmRpc.getState()\n .then((state) => {\n llmState.state = state;\n })\n .catch((error) => {\n console.error(\"Failed to get the initial state from the main process\", error);\n });\n"},{"path":["src","state","llmState.ts"],"content":"import {State} from \"lifecycle-utils\";\n\nimport {LlmState} from \"../../electron/state/llmState.ts\";\n\nexport const llmState = new State<LlmState>({\n appVersion: undefined,\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"},{"path":["src","utils","createRendererSideBirpc.ts"],"content":"import {createBirpc} from \"birpc\";\n\nexport function createRendererSideBirpc<\n const ElectronFunction = Record<string, never>,\n const RendererFunctions extends object = Record<string, never>\n>(\n toRendererEventName: string,\n fromRendererEventName: string,\n rendererFunctions: RendererFunctions\n) {\n return createBirpc<ElectronFunction, RendererFunctions>(rendererFunctions, {\n post: (data) => window.ipcRenderer.send(fromRendererEventName, data),\n on: (onData) => window.ipcRenderer.on(toRendererEventName, (event, data) => {\n onData(data);\n }),\n serialize: (value) => JSON.stringify(value),\n deserialize: (value) => JSON.parse(value)\n });\n}\n"},{"path":["src","vite-env.d.ts"],"content":"/// <reference types=\"vite/client\" />\n"},{"path":["tsconfig.json"],"content":"{\n \"compilerOptions\": {\n \"target\": \"es2022\",\n \"lib\": [\"es2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"es2022\",\n \"skipLibCheck\": true,\n\n \"esModuleInterop\": true,\n \"removeComments\": false,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"noUncheckedIndexedAccess\": true,\n \"moduleDetection\": \"force\",\n\n \"moduleResolution\": \"bundler\",\n \"allowImportingTsExtensions\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n\n \"strict\": true,\n \"strictNullChecks\": true,\n \"noImplicitAny\": true,\n \"noImplicitReturns\": true,\n \"noImplicitThis\": true,\n \"noImplicitOverride\": true\n },\n \"include\": [\n \"./src\",\n \"./electron\"\n ],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"path":["tsconfig.node.json"],"content":"{\n \"compilerOptions\": {\n \"target\": \"es2022\",\n \"lib\": [\"es2022\"],\n \"module\": \"es2022\",\n \"skipLibCheck\": true,\n\n \"esModuleInterop\": true,\n \"removeComments\": false,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"noUncheckedIndexedAccess\": true,\n \"moduleDetection\": \"force\",\n\n \"moduleResolution\": \"bundler\",\n \"resolveJsonModule\": false,\n \"isolatedModules\": true,\n \"composite\": true,\n\n \"strict\": true,\n \"strictNullChecks\": true,\n \"noImplicitAny\": true,\n \"noImplicitReturns\": true,\n \"noImplicitThis\": true,\n \"noImplicitOverride\": true\n },\n \"include\": [\n \"vite.config.ts\"\n ]\n}\n"},{"path":["vite.config.ts"],"content":"import path from \"node:path\";\nimport {fileURLToPath} from \"node:url\";\nimport {defineConfig} from \"vite\";\nimport electron from \"vite-plugin-electron/simple\";\nimport react from \"@vitejs/plugin-react\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// These modules won't be bundled as part of the Vite build of the Electron (main) side,\n// but they'll be included in the final Electron app build inside the asar file.\n// Performance and efficiency wise, this is absolutely fine and has no real drawbacks\nconst electronExternalModules = [\"node-llama-cpp\", \"lifecycle-utils\"];\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n esbuild: {\n target: \"es2022\"\n },\n optimizeDeps: {\n exclude: electronExternalModules,\n esbuildOptions: {\n target: \"es2022\"\n }\n },\n build: {\n outDir: path.join(__dirname, \"dist\"),\n target: \"es2022\"\n },\n root: path.join(__dirname, \"src\"),\n publicDir: path.join(__dirname, \"public\"),\n plugins: [\n react(),\n electron({\n main: {\n // Shortcut of `build.lib.entry`.\n entry: path.join(__dirname, \"electron/index.ts\"),\n onstart({startup}) {\n if (process.env[\"ENABLE_INSPECT\"] === \"true\")\n return startup([\".\", \"--inspect\"]);\n\n return startup([\".\"]);\n },\n vite: {\n build: {\n target: \"es2022\",\n outDir: path.join(__dirname, \"dist-electron\"),\n rollupOptions: {\n external: electronExternalModules\n }\n }\n }\n },\n preload: {\n // Shortcut of `build.rollupOptions.input`.\n // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.\n input: path.join(__dirname, \"electron/preload.ts\"),\n vite: {\n build: {\n target: \"es2022\",\n outDir: path.join(__dirname, \"dist-electron\")\n }\n }\n },\n // Polyfill the Electron and Node.js API for Renderer process.\n // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process.\n // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer\n renderer: process.env.NODE_ENV === \"test\"\n // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808\n ? undefined\n : {}\n })\n ]\n});\n"}]}
1
+ {"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}]\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 extends object = 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\";\nimport {defineConfig} from \"eslint/config\";\n\n\nexport default defineConfig({\n ignores: [\"dist/\", \"dist-electron/\", \"release/\", \"models/\"]\n}, {\n files: [\"**/**.{,c,m}{js,ts}{,x}\"],\n extends: [\n stylistic.configs[\"recommended\"],\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\": [\"warn\", 4],\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 ignoreUsingDeclarations: true\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 ignoreUsingDeclarations: true\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.8\",\n \"birpc\": \"^4.0.0\",\n \"classnames\": \"^2.5.1\",\n \"highlight.js\": \"^11.11.1\",\n \"lifecycle-utils\": \"^3.1.1\",\n \"markdown-it\": \"^14.1.1\",\n \"node-llama-cpp\": \"^{{currentNodeLlamaCppModuleVersion|escape}}\",\n \"pretty-ms\": \"^9.3.0\",\n \"react\": \"^19.2.4\",\n \"react-dom\": \"^19.2.4\",\n \"semver\": \"^7.7.1\"\n },\n \"devDependencies\": {\n \"@eslint/compat\": \"^2.0.2\",\n \"@stylistic/eslint-plugin\": \"^5.8.0\",\n \"@types/markdown-it\": \"^14.1.2\",\n \"@types/react\": \"^19.2.14\",\n \"@types/react-dom\": \"^19.2.3\",\n \"@types/semver\": \"^7.7.1\",\n \"@vitejs/plugin-react\": \"^5.1.4\",\n \"cross-env\": \"^10.1.0\",\n \"electron\": \"^40.4.1\",\n \"electron-builder\": \"^26.7.0\",\n \"eslint\": \"^9.39.2\",\n \"eslint-import-resolver-typescript\": \"^4.4.4\",\n \"eslint-plugin-import\": \"^2.32.0\",\n \"eslint-plugin-jsdoc\": \"^62.5.5\",\n \"eslint-plugin-n\": \"^17.24.0\",\n \"eslint-plugin-react-hooks\": \"^7.0.1\",\n \"eslint-plugin-react-refresh\": \"^0.5.0\",\n \"rimraf\": \"^6.1.3\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.56.0\",\n \"vite\": \"^7.3.1\",\n \"vite-plugin-electron\": \"^0.29.0\",\n \"vite-plugin-electron-renderer\": \"^0.14.6\",\n \"zx\": \"^8.8.5\"\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 --app-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: 8px;\n color: var(--star-link-color);\n\n &:hover {\n color: var(--star-hover-color);\n }\n\n > .starIcon {\n flex-shrink: 0;\n fill: currentColor;\n }\n }\n\n > .separator {\n height: 1px;\n background-color: var(--actions-block-border-color);\n margin: 0px -24px;\n align-self: stretch;\n }\n\n > .title {\n padding-inline-end: 16px;\n color: var(--text-color);\n opacity: 0.8;\n font-weight: 600;\n }\n\n > .links {\n display: flex;\n flex-direction: row;\n\n > a {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n > .downloadIcon {\n flex-shrink: 0;\n fill: currentColor;\n }\n }\n\n > .separator {\n width: 1px;\n background-color: var(--link-color);\n opacity: 0.2;\n margin: 0px 16px;\n height: 0.8lh;\n align-self: center;\n }\n }\n\n > .browseLink {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n > .searchIcon {\n flex-shrink: 0;\n fill: currentColor;\n }\n }\n }\n }\n\n > .loading {\n opacity: 0.6;\n font-weight: bold;\n\n mask: linear-gradient(\n to right,\n rgb(0 0 0 / 48%) 34%,\n black,\n rgb(0 0 0 / 48%) 66%\n ) content-box 0 0 / 300% 100% no-repeat;\n animation: loading-animation 2s infinite ease-in-out;\n }\n\n > .typeMessage {\n opacity: 0.6;\n }\n }\n}\n\n@keyframes loading-animation {\n 0% {\n mask-position: 100% 100%;\n }\n\n 100% {\n mask-position: 0 100%;\n }\n}\n"},{"path":["src","App","App.tsx"],"content":"import {useCallback, useLayoutEffect, useRef} from \"react\";\nimport {llmState} from \"../state/llmState.ts\";\nimport {electronLlmRpc} from \"../rpc/llmRpc.ts\";\nimport {useExternalState} from \"../hooks/useExternalState.ts\";\nimport {SearchIconSVG} from \"../icons/SearchIconSVG.tsx\";\nimport {StarIconSVG} from \"../icons/StarIconSVG.tsx\";\nimport {DownloadIconSVG} from \"../icons/DownloadIconSVG.tsx\";\nimport {Header} from \"./components/Header/Header.tsx\";\nimport {ChatHistory} from \"./components/ChatHistory/ChatHistory.tsx\";\nimport {InputRow} from \"./components/InputRow/InputRow.tsx\";\n\nimport \"./App.css\";\n\n\nexport function App() {\n const state = useExternalState(llmState);\n const {generatingResult} = state.chatSession;\n const isScrollAnchoredRef = useRef(false);\n const lastAnchorScrollTopRef = useRef<number>(0);\n\n const isScrolledToTheBottom = useCallback(() => {\n return (\n document.documentElement.scrollHeight - document.documentElement.scrollTop - 1\n ) <= document.documentElement.clientHeight;\n }, []);\n\n const scrollToBottom = useCallback(() => {\n const newScrollTop = document.documentElement.scrollHeight - document.documentElement.clientHeight;\n\n if (newScrollTop > document.documentElement.scrollTop && newScrollTop > lastAnchorScrollTopRef.current) {\n document.documentElement.scrollTo({\n top: newScrollTop,\n behavior: \"smooth\"\n });\n lastAnchorScrollTopRef.current = document.documentElement.scrollTop;\n }\n\n isScrollAnchoredRef.current = true;\n }, []);\n\n useLayoutEffect(() => {\n // anchor scroll to bottom\n\n function onScroll() {\n const currentScrollTop = document.documentElement.scrollTop;\n\n isScrollAnchoredRef.current = isScrolledToTheBottom() ||\n currentScrollTop >= lastAnchorScrollTopRef.current;\n\n // handle scroll animation\n if (isScrollAnchoredRef.current)\n lastAnchorScrollTopRef.current = currentScrollTop;\n }\n\n const observer = new ResizeObserver(() => {\n if (isScrollAnchoredRef.current && !isScrolledToTheBottom())\n scrollToBottom();\n });\n\n window.addEventListener(\"scroll\", onScroll, {passive: false});\n observer.observe(document.body, {\n box: \"border-box\"\n });\n scrollToBottom();\n\n return () => {\n observer.disconnect();\n window.removeEventListener(\"scroll\", onScroll);\n };\n }, []);\n\n const openSelectModelFileDialog = useCallback(async () => {\n await electronLlmRpc.selectModelFileAndLoad();\n }, []);\n\n const stopActivePrompt = useCallback(() => {\n void electronLlmRpc.stopActivePrompt();\n }, []);\n\n const resetChatHistory = useCallback(() => {\n void electronLlmRpc.stopActivePrompt();\n void electronLlmRpc.resetChatHistory();\n }, []);\n\n const sendPrompt = useCallback((prompt: string) => {\n if (generatingResult)\n return;\n\n scrollToBottom();\n void electronLlmRpc.prompt(prompt);\n }, [generatingResult, scrollToBottom]);\n\n const onPromptInput = useCallback((currentText: string) => {\n void electronLlmRpc.setDraftPrompt(currentText);\n }, []);\n\n const error = state.llama.error ?? state.model.error ?? state.context.error ?? state.contextSequence.error;\n const loading = state.selectedModelFilePath != null && error == null && (\n !state.model.loaded || !state.llama.loaded || !state.context.loaded || !state.contextSequence.loaded || !state.chatSession.loaded\n );\n const showMessage = state.selectedModelFilePath == null || error != null || state.chatSession.simplifiedChat.length === 0;\n\n return <div className=\"app\">\n <Header\n appVersion={state.appVersion}\n canShowCurrentVersion={state.selectedModelFilePath == null}\n modelName={state.model.name}\n loadPercentage={state.model.loadProgress}\n onLoadClick={openSelectModelFileDialog}\n onResetChatClick={\n !showMessage\n ? resetChatHistory\n : undefined\n }\n />\n {\n showMessage &&\n <div className=\"message\">\n {\n error != null &&\n <div className=\"error\">\n {String(error)}\n </div>\n }\n {\n loading &&\n <div className=\"loading\">\n Loading...\n </div>\n }\n {\n (state.selectedModelFilePath == null || state.llama.error != null) &&\n <div className=\"loadModel\">\n <div className=\"hint\">Click the button above to load a model</div>\n <div className=\"actions\">\n <a className=\"starLink\" target=\"_blank\" href=\"https://github.com/withcatai/node-llama-cpp\">\n <StarIconSVG className=\"starIcon\" />\n <div className=\"text\">\n Star <code>node-llama-cpp</code> on GitHub\n </div>\n </a>\n\n <div className=\"separator\"></div>\n <div className=\"title\">DeepSeek R1 Distill Qwen model</div>\n <div className=\"links\">\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/mradermacher/DeepSeek-R1-Distill-Qwen-7B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-7B.Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get 7B</div>\n </a>\n <div className=\"separator\" />\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/mradermacher/DeepSeek-R1-Distill-Qwen-14B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-14B.Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get 14B</div>\n </a>\n <div className=\"separator\" />\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/mradermacher/DeepSeek-R1-Distill-Qwen-32B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-32B.Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get 32B</div>\n </a>\n </div>\n\n <div className=\"separator\"></div>\n <div className=\"title\">Other models</div>\n <div className=\"links\">\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/giladgd/gpt-oss-20b-GGUF/resolve/main/gpt-oss-20b.MXFP4.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\"><code>gpt-oss</code> 20B</div>\n </a>\n <div className=\"separator\" />\n <a\n target=\"_blank\"\n href=\"https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q4_K_M.gguf\"\n >\n <DownloadIconSVG className=\"downloadIcon\" />\n <div className=\"text\">Get Gemma 2 2B</div>\n </a>\n </div>\n\n <div className=\"separator\"></div>\n <a className=\"browseLink\" target=\"_blank\" href=\"https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending\">\n <SearchIconSVG className=\"searchIcon\" />\n <div className=\"text\">Find more models</div>\n </a>\n </div>\n </div>\n }\n {\n (\n !loading &&\n state.selectedModelFilePath != null &&\n error == null &&\n state.chatSession.simplifiedChat.length === 0\n ) &&\n <div className=\"typeMessage\">\n Type a message to start the conversation\n </div>\n }\n </div>\n }\n {\n !showMessage &&\n <ChatHistory\n className=\"chatHistory\"\n simplifiedChat={state.chatSession.simplifiedChat}\n generatingResult={generatingResult}\n />\n }\n <InputRow\n disabled={!state.model.loaded || !state.contextSequence.loaded}\n stopGeneration={\n generatingResult\n ? stopActivePrompt\n : undefined\n }\n onPromptInput={onPromptInput}\n sendPrompt={sendPrompt}\n generatingResult={generatingResult}\n autocompleteInputDraft={state.chatSession.draftPrompt.prompt}\n autocompleteCompletion={state.chatSession.draftPrompt.completion}\n />\n </div>;\n}\n"},{"path":["src","App","components","ChatHistory","ChatHistory.css"],"content":".appChatHistory {\n flex: 1;\n display: flex;\n flex-direction: column;\n text-align: start;\n overflow: auto;\n padding: 24px 0px;\n}\n"},{"path":["src","App","components","ChatHistory","ChatHistory.tsx"],"content":"import {useMemo} from \"react\";\nimport classNames from \"classnames\";\nimport {LlmState, SimplifiedModelChatItem} from \"../../../../electron/state/llmState.ts\";\nimport {UserMessage} from \"./components/UserMessage/UserMessage.js\";\nimport {ModelMessage} from \"./components/ModelMessage/ModelMessage.js\";\n\nimport \"./ChatHistory.css\";\n\n\nexport function ChatHistory({simplifiedChat, generatingResult, className}: ChatHistoryProps) {\n const renderChatItems = useMemo(() => {\n if (simplifiedChat.length > 0 &&\n simplifiedChat.at(-1)!.type !== \"model\" &&\n generatingResult\n )\n return [...simplifiedChat, emptyModelMessage];\n\n return simplifiedChat;\n }, [simplifiedChat, generatingResult]);\n\n return <div className={classNames(\"appChatHistory\", className)}>\n {\n renderChatItems\n .map((item, index) => {\n if (item.type === \"model\")\n return <ModelMessage\n key={index}\n modelMessage={item}\n active={index === renderChatItems.length - 1 && generatingResult}\n />;\n else if (item.type === \"user\")\n return <UserMessage key={index} message={item} />;\n\n return null;\n })\n }\n </div>;\n}\n\ntype ChatHistoryProps = {\n simplifiedChat: LlmState[\"chatSession\"][\"simplifiedChat\"],\n generatingResult: boolean,\n className?: string\n};\n\nconst emptyModelMessage: SimplifiedModelChatItem = {\n type: \"model\",\n message: []\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","ModelMessage.css"],"content":".appChatHistory > .message.model {\n align-self: flex-start;\n margin-inline-end: 48px;\n padding-inline-start: 18px;\n word-break: break-word;\n max-width: calc(100% - 48px);\n box-sizing: border-box;\n min-height: fit-content;\n interpolate-size: allow-keywords;\n\n transition: min-height 0.5s var(--transition-easing), max-height 0.5s var(--transition-easing);\n\n &:hover + .buttons {\n opacity: 1;\n }\n\n &:last-child {\n margin-bottom: 0px;\n min-height: calc(50svh);\n\n @starting-style {\n min-height: 0px;\n }\n }\n\n > .text {\n padding: 0px 6px;\n }\n\n > .buttons {\n display: flex;\n flex-direction: row;\n padding: 12px 0px 8px 0px;\n opacity: 0.6;\n justify-self: flex-start;\n\n transition: opacity 0.1s ease-in-out;\n\n &:hover,\n &:focus-visible {\n opacity: 1;\n }\n\n &[inert] {\n opacity: 0;\n }\n }\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","ModelMessage.tsx"],"content":"import {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {SimplifiedModelChatItem} from \"../../../../../../electron/state/llmState.js\";\nimport {ModelResponseThought} from \"../ModelResponseThought/ModelResponseThought.js\";\nimport {ModelResponseComment} from \"../ModelResponseComment/ModelResponseComment.js\";\nimport {ModelMessageCopyButton} from \"./components/ModelMessageCopyButton/ModelMessageCopyButton.js\";\n\nimport \"./ModelMessage.css\";\n\nexport function ModelMessage({modelMessage, active}: ModelMessageProps) {\n return <div className=\"message model\">\n {\n modelMessage.message.map((message, responseIndex) => {\n const isLastMessage = responseIndex === modelMessage.message.length - 1;\n\n if (message.type === \"segment\") {\n if (message.segmentType === \"thought\")\n return <ModelResponseThought\n key={responseIndex}\n text={message.text}\n active={isLastMessage && active}\n duration={\n (message.startTime != null && message.endTime != null)\n ? (new Date(message.endTime).getTime() - new Date(message.startTime).getTime())\n : undefined\n }\n />;\n else if (message.segmentType === \"comment\")\n return <ModelResponseComment\n key={responseIndex}\n text={message.text}\n active={isLastMessage && active}\n />;\n else\n // ensure we handle all segment types or TypeScript will complain\n void (message.segmentType satisfies never);\n }\n\n return <MessageMarkdown\n key={responseIndex}\n activeDot={isLastMessage && active}\n className=\"text\"\n >\n {message.text}\n </MessageMarkdown>;\n })\n }\n {\n (modelMessage.message.length === 0 && active) &&\n <MessageMarkdown className=\"text\" activeDot />\n }\n <div className=\"buttons\" inert={active}>\n <ModelMessageCopyButton modelMessage={modelMessage.message} />\n </div>\n </div>;\n}\n\ntype ModelMessageProps = {\n modelMessage: SimplifiedModelChatItem,\n active: boolean\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","components","ModelMessageCopyButton","ModelMessageCopyButton.css"],"content":".appChatHistory > .message.model > .buttons {\n > .copyButton {\n display: grid;\n grid-template-areas: \"icon\";\n padding: 6px;\n border: none;\n\n transition: background-color 0.1s ease-in-out;\n\n &:not(:hover, :focus-visible) {\n background-color: transparent;\n }\n\n &.copied {\n > .icon.copy {\n opacity: 0;\n transition-delay: 0s;\n }\n\n > .icon.check {\n opacity: 1;\n transition-delay: 0.1s;\n }\n }\n\n > .icon {\n grid-area: icon;\n width: 18px;\n height: 18px;\n\n transition: opacity 0.3s ease-in-out;\n\n &.copy {\n opacity: 1;\n transition-delay: 0.1s;\n }\n &.check {\n opacity: 0;\n }\n }\n }\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelMessage","components","ModelMessageCopyButton","ModelMessageCopyButton.tsx"],"content":"import classNames from \"classnames\";\nimport {useCallback, useState} from \"react\";\nimport {CopyIconSVG} from \"../../../../../../../icons/CopyIconSVG.js\";\nimport {CheckIconSVG} from \"../../../../../../../icons/CheckIconSVG.js\";\nimport {SimplifiedModelChatItem} from \"../../../../../../../../electron/state/llmState.js\";\n\nimport \"./ModelMessageCopyButton.css\";\n\nconst showCopiedTime = 1000 * 2;\n\nexport function ModelMessageCopyButton({modelMessage}: ModelMessageCopyButtonProps) {\n const [copies, setCopies] = useState(0);\n\n const onClick = useCallback(() => {\n const text = modelMessage\n .filter((item) => item.type === \"text\")\n .map((item) => item.text)\n .join(\"\\n\")\n .trim();\n\n navigator.clipboard.writeText(text)\n .then(() => {\n setCopies((copies) => copies + 1);\n\n setTimeout(() => {\n setCopies((copies) => copies - 1);\n }, showCopiedTime);\n })\n .catch((error) => {\n console.error(\"Failed to copy text to clipboard\", error);\n });\n }, [modelMessage]);\n\n return <button\n onClick={onClick}\n className={classNames(\"copyButton\", copies > 0 && \"copied\")}\n >\n <CopyIconSVG className=\"icon copy\" />\n <CheckIconSVG className=\"icon check\" />\n </button>;\n}\n\ntype ModelMessageCopyButtonProps = {\n modelMessage: SimplifiedModelChatItem[\"message\"]\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseComment","ModelResponseComment.css"],"content":".appChatHistory > .message.model > .responseComment {\n padding: 4px 16px 0px 16px;\n position: relative;\n margin: 4px -8px;\n border-radius: 12px;\n\n transition: margin-bottom 0.3s var(--transition-easing), background-color 0.5s var(--transition-easing);\n\n &.active {\n margin-bottom: 8px;\n\n > .header > .opener > .summary {\n opacity: 1;\n\n > .title {\n opacity: 0.6;\n font-weight: bold;\n --generating-animation-mask-transparency-color: rgb(0 0 0 / 48%);\n\n animation-play-state: running;\n }\n\n > .chevron {\n opacity: 0.48;\n }\n }\n }\n\n &.open {\n margin-bottom: 20px;\n background-color: var(--model-comment-block-background-color);\n\n > .header > .opener > .summary > .chevron {\n transform: rotate(90deg);\n margin-inline-end: -2px;\n }\n }\n\n > .header {\n display: flex;\n flex-direction: row;\n\n > .opener {\n border: none;\n background-color: var(--model-comment-block-button-background-color);\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n margin: 0px 0px 0px -12px;\n border-radius: 12px;\n user-select: none;\n outline: solid 2px transparent;\n outline-offset: 4px;\n align-self: flex-start;\n max-width: 100%;\n opacity: 0.64;\n\n transition: opacity 0.3s var(--transition-easing);\n\n &:focus-visible {\n outline: solid 2px Highlight;\n outline-offset: 0px;\n }\n\n &:hover {\n opacity: 0.82;\n }\n\n > .summary {\n display: flex;\n flex-direction: row;\n align-items: center;\n\n > .title {\n --generating-animation-mask-transparency-color: rgb(0 0 0 / 100%);\n transition: font-weight 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing), --generating-animation-mask-transparency-color 0.3s var(--transition-easing), margin-bottom 0.3s var(--transition-easing);\n mask: linear-gradient(\n to right,\n var(--generating-animation-mask-transparency-color) 34%,\n black,\n var(--generating-animation-mask-transparency-color) 66%\n ) content-box 0 0 / 300% 100% no-repeat;\n animation: generating-animation 2s infinite ease-in-out;\n animation-play-state: paused;\n white-space: nowrap;\n }\n\n > .chevron {\n flex-shrink: 0;\n\n width: 20px;\n height: 20px;\n margin: -4px;\n margin-inline-start: 0px;\n margin-inline-end: -6px;\n opacity: 0.64;\n\n transform-origin: 56% 56%;\n transition: transform 0.2s var(--transition-easing), margin-inline-end 0.2s var(--transition-easing), opacity 0.3s var(--transition-easing);\n }\n }\n }\n\n > .excerpt {\n white-space: nowrap;\n overflow: hidden;\n display: flex;\n justify-content: end;\n align-self: center;\n width: calc-size(fit-content, min(360px, size + 8px));\n opacity: 0.24;\n mask: linear-gradient(to right, transparent, black 64px);\n margin-inline-start: 4px;\n\n user-select: none;\n\n interpolate-size: allow-keywords;\n transition: margin-inline-start 0.2s var(--transition-easing), width 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &.hide {\n width: calc-size(fit-content, max(0px, min(360px, size + 8px) - 24px));\n opacity: 0;\n margin-inline-start: 0px; /* this is to offset the chevron right margin on open */\n transition-delay: 0s, 0s;\n }\n }\n }\n\n > .comment {\n margin-top: 16px;\n padding-bottom: 12px;\n display: flex;\n flex-direction: column;\n interpolate-size: allow-keywords;\n transition: height 0.5s var(--transition-easing), margin-top 0.5s var(--transition-easing), padding-bottom 0.5s var(--transition-easing), margin-bottom 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &.hide {\n margin-top: 32px;\n height: 0px;\n margin-bottom: -12px;\n padding-bottom: 0px;\n opacity: 0;\n transition-delay: 0s, 0s, 0s, 0s;\n }\n\n > .content {\n opacity: 0.64;\n justify-self: flex-start;\n position: relative;\n overflow: hidden;\n max-height: 100%;\n }\n }\n}\n\n@keyframes generating-animation {\n 0% {\n mask-position: 100% 100%;\n }\n\n 100% {\n mask-position: 0 100%;\n }\n}\n\n@property --generating-animation-mask-transparency-color {\n syntax: \"<color>\";\n inherits: false;\n initial-value: transparent;\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseComment","ModelResponseComment.tsx"],"content":"import classNames from \"classnames\";\nimport {useCallback, useMemo, useState} from \"react\";\nimport {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {RightChevronIconSVG} from \"../../../../../icons/RightChevronIconSVG.js\";\nimport {MarkdownContent} from \"../../../MarkdownContent/MarkdownContent.js\";\n\nimport \"./ModelResponseComment.css\";\n\nconst excerptLength = 1024;\n\nexport function ModelResponseComment({text, active}: ModelResponseCommentProps) {\n const [isOpen, setIsOpen] = useState(false);\n\n const toggleIsOpen = useCallback(() => {\n setIsOpen((isOpen) => !isOpen);\n }, []);\n\n const title = useMemo(() => {\n if (active)\n return \"Generating comment\";\n\n return \"Generated comment\";\n }, [active]);\n\n return <div className={classNames(\"responseComment\", active && \"active\", isOpen && \"open\")}>\n <div className=\"header\">\n <button className=\"opener\" onClick={toggleIsOpen}>\n <span className=\"summary\">\n <div className=\"title\">{title}</div>\n <RightChevronIconSVG className=\"chevron\" />\n\n </span>\n </button>\n <MarkdownContent\n className={classNames(\"excerpt\", isOpen && \"hide\")}\n dir=\"auto\"\n inline\n >\n {text.slice(-excerptLength)}\n </MarkdownContent>\n </div>\n <div className={classNames(\"comment\", !isOpen && \"hide\")}>\n <MessageMarkdown className=\"content\" activeDot={active}>{text}</MessageMarkdown>\n </div>\n </div>;\n}\n\ntype ModelResponseCommentProps = {\n text: string,\n active: boolean\n};\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseThought","ModelResponseThought.css"],"content":".appChatHistory > .message.model > .responseThought {\n padding: 0px 8px;\n\n transition: margin-bottom 0.3s var(--transition-easing);\n\n &.active {\n margin-bottom: 8px;\n\n > .header {\n > .summary {\n opacity: 1;\n\n > .title {\n opacity: 0.6;\n font-weight: bold;\n --thinking-animation-mask-transparency-color: rgb(0 0 0 / 48%);\n\n animation-play-state: running;\n }\n\n > .chevron {\n opacity: 0.48;\n }\n }\n }\n }\n\n &.open {\n > .header {\n > .summary {\n > .chevron {\n transform: rotate(90deg);\n }\n }\n }\n }\n\n > .header {\n border: none;\n background-color: transparent;\n display: flex;\n flex-direction: column;\n padding: 0px;\n user-select: none;\n outline: solid 2px transparent;\n border-radius: 4px;\n outline-offset: 4px;\n align-self: flex-start;\n max-width: 100%;\n\n &:focus-visible {\n outline: solid 2px Highlight;\n }\n\n &:hover > .summary {\n opacity: 1;\n }\n\n > .summary {\n display: flex;\n flex-direction: row;\n align-items: center;\n opacity: 0.64;\n\n transition: opacity 0.3s var(--transition-easing);\n\n > .title {\n --thinking-animation-mask-transparency-color: rgb(0 0 0 / 100%);\n transition: font-weight 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing), --thinking-animation-mask-transparency-color 0.3s var(--transition-easing), margin-bottom 0.3s var(--transition-easing);\n mask: linear-gradient(\n to right,\n var(--thinking-animation-mask-transparency-color) 34%,\n black,\n var(--thinking-animation-mask-transparency-color) 66%\n ) content-box 0 0 / 300% 100% no-repeat;\n animation: thinking-animation 2s infinite ease-in-out;\n animation-play-state: paused;\n }\n\n > .chevron {\n flex-shrink: 0;\n\n width: 20px;\n height: 20px;\n margin: -4px;\n margin-inline-start: 0px;\n opacity: 0.64;\n\n transform-origin: 56% 56%;\n transition: transform 0.2s var(--transition-easing), opacity 0.3s var(--transition-easing);\n }\n }\n\n > .excerpt {\n white-space: nowrap;\n overflow: hidden;\n display: flex;\n justify-content: end;\n justify-self: flex-start;\n mask: linear-gradient(to right, transparent, black 48px);\n max-width: calc-size(fit-content, min(360px, size + 8px));\n opacity: 0.24;\n font-size: 14px;\n margin-top: 2px;\n user-select: none;\n padding-inline-end: 24px;\n\n interpolate-size: allow-keywords;\n transition: height 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &.hide {\n height: 0px;\n opacity: 0;\n transition-delay: 0s, 0s;\n }\n }\n }\n\n > .content {\n margin-top: 16px;\n margin-bottom: 24px;\n opacity: 0.64;\n padding-left: 24px;\n justify-self: flex-start;\n position: relative;\n overflow: clip;\n\n interpolate-size: allow-keywords;\n transition: height 0.5s var(--transition-easing), margin-bottom 0.5s var(--transition-easing), opacity 0.3s 0.2s var(--transition-easing);\n\n &:before {\n content: \"\";\n position: absolute;\n width: 4px;\n height: 100%;\n background-color: var(--message-blockquote-border-color);\n left: 0px;\n }\n\n &.hide {\n height: 0px;\n margin-bottom: 0px;\n opacity: 0;\n transition-delay: 0s, 0s;\n }\n }\n}\n\n@keyframes thinking-animation {\n 0% {\n mask-position: 100% 100%;\n }\n\n 100% {\n mask-position: 0 100%;\n }\n}\n\n@property --thinking-animation-mask-transparency-color {\n syntax: \"<color>\";\n inherits: false;\n initial-value: transparent;\n}\n"},{"path":["src","App","components","ChatHistory","components","ModelResponseThought","ModelResponseThought.tsx"],"content":"import classNames from \"classnames\";\nimport {useCallback, useMemo, useState} from \"react\";\nimport prettyMilliseconds from \"pretty-ms\";\nimport {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {RightChevronIconSVG} from \"../../../../../icons/RightChevronIconSVG.js\";\nimport {MarkdownContent} from \"../../../MarkdownContent/MarkdownContent.js\";\n\nimport \"./ModelResponseThought.css\";\n\nconst excerptLength = 1024;\n\nexport function ModelResponseThought({text, active, duration}: ModelResponseThoughtProps) {\n const [isOpen, setIsOpen] = useState(false);\n\n const toggleIsOpen = useCallback(() => {\n setIsOpen((isOpen) => !isOpen);\n }, []);\n\n const title = useMemo(() => {\n if (active)\n return \"Thinking\";\n else if (duration != null) {\n const formattedDuration = prettyMilliseconds(duration, {\n secondsDecimalDigits: duration < 1000 * 10 ? 2 : 0,\n verbose: true\n });\n return `Thought for ${formattedDuration}`;\n }\n\n return \"Finished thinking\";\n }, [active, duration]);\n\n return <div className={classNames(\"responseThought\", active && \"active\", isOpen && \"open\")}>\n <button className=\"header\" onClick={toggleIsOpen}>\n <span className=\"summary\">\n <div className=\"title\">{title}</div>\n <RightChevronIconSVG className=\"chevron\" />\n </span>\n <MarkdownContent\n className={classNames(\"excerpt\", isOpen && \"hide\")}\n dir=\"auto\"\n inline\n >\n {text.slice(-excerptLength)}\n </MarkdownContent>\n </button>\n <MessageMarkdown className={classNames(\"content\", !isOpen && \"hide\")} activeDot={active}>{text}</MessageMarkdown>\n </div>;\n}\n\ntype ModelResponseThoughtProps = {\n text: string,\n active: boolean,\n duration?: number\n};\n"},{"path":["src","App","components","ChatHistory","components","UserMessage","UserMessage.css"],"content":".appChatHistory > .message.user {\n align-self: flex-end;\n background-color: var(--user-message-background-color);\n padding: 8px 12px;\n border-radius: 12px;\n margin-top: 0px;\n margin-bottom: 16px;\n margin-inline-start: 48px;\n margin-inline-end: 12px;\n color: var(--user-message-text-color);\n word-break: break-word;\n max-width: calc(100% - 48px - 12px);\n box-sizing: border-box;\n\n &:not(:first-child) {\n margin-top: 36px;\n }\n}\n"},{"path":["src","App","components","ChatHistory","components","UserMessage","UserMessage.tsx"],"content":"import {MessageMarkdown} from \"../../../MessageMarkdown/MessageMarkdown.js\";\nimport {SimplifiedUserChatItem} from \"../../../../../../electron/state/llmState.js\";\n\nimport \"./UserMessage.css\";\n\nexport function UserMessage({message}: UserMessageProps) {\n return <MessageMarkdown className=\"message user\">\n {message.message}\n </MessageMarkdown>;\n}\n\ntype UserMessageProps = {\n message: SimplifiedUserChatItem\n};\n"},{"path":["src","App","components","FixedDivWithSpacer","FixedDivWithSpacer.tsx"],"content":"import React, {useLayoutEffect, useRef} from \"react\";\nimport classNames from \"classnames\";\n\nexport function FixedDivWithSpacer({className, ...props}: FixedDivWithSpacerProps) {\n const spacerRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n if (spacerRef.current == null)\n return;\n\n const spacerTag = spacerRef.current;\n const mainTag = spacerTag.previousElementSibling as HTMLDivElement | null;\n\n if (mainTag == null)\n return;\n\n const resizeObserver = new ResizeObserver(() => {\n spacerTag.style.width = `${mainTag.offsetWidth}px`;\n spacerTag.style.height = `${mainTag.offsetHeight}px`;\n });\n resizeObserver.observe(mainTag, {\n box: \"content-box\"\n });\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [spacerRef]);\n\n return <>\n <div className={classNames(className, \"main\")} {...props} />\n <div ref={spacerRef} className={classNames(className, \"spacer\")} />\n </>;\n}\n\ntype DivProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;\ntype FixedDivWithSpacerProps = DivProps;\n"},{"path":["src","App","components","Header","Header.css"],"content":".appHeader {\n display: flex;\n flex-direction: row;\n top: 16px;\n pointer-events: none;\n\n &.spacer {\n position: sticky;\n }\n\n &.main {\n width: calc(100% - 16px * 2);\n max-width: var(--app-max-width, 1280px);\n position: fixed;\n z-index: 10;\n\n > .panel {\n pointer-events: all;\n display: flex;\n flex-direction: row;\n align-self: start;\n background-color: var(--panel-background-color);\n border-radius: 12px;\n backdrop-filter: blur(8px);\n box-shadow: var(--panel-box-shadow);\n overflow: clip;\n isolation: isolate;\n color: var(--panel-text-color);\n z-index: 10;\n\n > button {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 8px 12px;\n margin: 8px;\n background-color: var(--panel-button-background-color);\n color: var(--panel-text-color);\n fill: var(--panel-text-color);\n\n + button {\n margin-inline-start: 0px;\n }\n\n &:hover,\n &:focus,\n &:focus-visible {\n border-color: var(--panel-button-hover-border-color);\n }\n\n > .icon {\n width: 20px;\n height: 20px;\n }\n }\n }\n\n > .model {\n position: relative;\n\n > .progress {\n position: absolute;\n inset-inline-start: 0;\n top: 0;\n bottom: 0;\n background-color: var(--panel-progress-color);\n width: calc(var(--progress) * 100%);\n pointer-events: none;\n z-index: -1;\n\n --progress: 0;\n\n &.hide {\n opacity: 0;\n\n transition: opacity 0.3s var(--transition-easing);\n }\n }\n\n > .modelName,\n > .noModel {\n flex: 1;\n text-align: start;\n align-self: center;\n flex-basis: 400px;\n padding: 12px 24px;\n word-break: break-word;\n\n margin-inline-end: 48px;\n }\n }\n\n > .spacer {\n flex-grow: 1;\n }\n }\n}\n"},{"path":["src","App","components","Header","Header.tsx"],"content":"import {CSSProperties} from \"react\";\nimport classNames from \"classnames\";\nimport {LoadFileIconSVG} from \"../../../icons/LoadFileIconSVG.tsx\";\nimport {DeleteIconSVG} from \"../../../icons/DeleteIconSVG.tsx\";\nimport {FixedDivWithSpacer} from \"../FixedDivWithSpacer/FixedDivWithSpacer.tsx\";\nimport {UpdateBadge} from \"./components/UpdateBadge.tsx\";\n\nimport \"./Header.css\";\n\n\nexport function Header({appVersion, canShowCurrentVersion, modelName, onLoadClick, loadPercentage, onResetChatClick}: HeaderProps) {\n // we use a FixedDivWithSpacer to push down the content while keeping the header fixed.\n // this allows the content to have macOS's scroll bounce while keeping the header fixed at the top.\n return <FixedDivWithSpacer className=\"appHeader\">\n <div className=\"panel model\">\n <div\n className={classNames(\"progress\", loadPercentage === 1 && \"hide\")}\n style={{\n \"--progress\": loadPercentage != null ? (loadPercentage * 100) : undefined\n } as CSSProperties}\n />\n\n {\n modelName != null &&\n <div className=\"modelName\">{modelName}</div>\n }\n {\n modelName == null &&\n <div className=\"noModel\">No model loaded</div>\n }\n\n <button\n className=\"resetChatButton\"\n disabled={onResetChatClick == null}\n onClick={onResetChatClick}\n >\n <DeleteIconSVG className=\"icon\" />\n </button>\n <button className=\"loadModelButton\" onClick={onLoadClick} disabled={onLoadClick == null}>\n <LoadFileIconSVG className=\"icon\" />\n </button>\n </div>\n <div className=\"spacer\" />\n <UpdateBadge\n appVersion={appVersion}\n canShowCurrentVersion={canShowCurrentVersion}\n />\n </FixedDivWithSpacer>;\n}\n\ntype HeaderProps = {\n appVersion?: string,\n canShowCurrentVersion?: boolean,\n modelName?: string,\n onLoadClick?(): void,\n loadPercentage?: number,\n onResetChatClick?(): void\n};\n"},{"path":["src","App","components","Header","components","UpdateBadge.css"],"content":".appHeader > .updateBadge {\n pointer-events: all;\n display: flex;\n flex-direction: row;\n align-items: center;\n flex-shrink: 0;\n margin-inline-start: 16px;\n\n > .currentVersion {\n opacity: 0.4;\n margin: 14px;\n\n > code {\n background-color: transparent;\n }\n }\n\n > .newVersion {\n background-color: var(--update-badge-background-color);\n border: solid 1px var(--update-badge-border-color);\n box-shadow: var(--update-badge-box-shadow);\n border-radius: 12px;\n backdrop-filter: blur(8px);\n padding: 8px 12px;\n margin: 0px 8px;\n\n > .version {\n margin: 0px 4px;\n font-family: monospace;\n }\n }\n}\n"},{"path":["src","App","components","Header","components","UpdateBadge.tsx"],"content":"import {useCallback, useEffect, useMemo, useRef, useState} from \"react\";\nimport {withLock} from \"lifecycle-utils\";\nimport semver from \"semver\";\n\nimport \"./UpdateBadge.css\";\n\nconst latestReleaseUrl = \"https://github.com/withcatai/node-llama-cpp/releases/latest\";\nconst checkInterval = 1000 * 60 * 60 * 24;\n\n\nexport function UpdateBadge({appVersion, canShowCurrentVersion}: UpdateBadgeProps) {\n const [latestVersion, setLatestVersion] = useState<string | null>(null);\n const [releaseLink, setReleaseLink] = useState<string | null>(null);\n const shouldUpdateCurrentVersion = useRef(true);\n const nextUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const instanceLock = useRef({});\n\n const appVersionIsBeta = useMemo(() => {\n if (appVersion == null)\n return null;\n\n const componenets = semver.prerelease(appVersion);\n return componenets?.includes(\"beta\") ?? false;\n }, [appVersion]);\n\n const updateLatestVersionInfo = useCallback(async function updateLatestVersionInfo() {\n clearTimeout(nextUpdateTimeoutRef.current);\n await withLock([instanceLock.current, \"updateVersion\"], async () => {\n clearTimeout(nextUpdateTimeoutRef.current);\n\n const latestVersion = await getLatestAvailableVersion(appVersionIsBeta ?? false);\n if (shouldUpdateCurrentVersion.current && latestVersion.version != null) {\n setLatestVersion(latestVersion.version);\n setReleaseLink(latestVersion.url);\n }\n\n nextUpdateTimeoutRef.current = setTimeout(updateLatestVersionInfo, checkInterval);\n });\n }, [appVersionIsBeta]);\n\n useEffect(() => {\n if (appVersionIsBeta == null)\n return;\n\n shouldUpdateCurrentVersion.current = true;\n void updateLatestVersionInfo();\n\n return () => {\n shouldUpdateCurrentVersion.current = false;\n clearTimeout(nextUpdateTimeoutRef.current);\n };\n }, [appVersionIsBeta]);\n\n const releasedVersionIsNewerThanCurrent = useMemo(() => {\n if (appVersion == null || latestVersion == null)\n return false;\n\n try {\n return semver.gt(latestVersion, appVersion);\n } catch (err) {\n return true;\n }\n }, [appVersion, latestVersion]);\n\n if (latestVersion == null)\n return null;\n\n return <div className=\"updateBadge\">\n {\n (!releasedVersionIsNewerThanCurrent && appVersion && canShowCurrentVersion) &&\n <div className=\"currentVersion\"><code>v{appVersion}</code></div>\n }\n {\n (releasedVersionIsNewerThanCurrent && releaseLink != null) &&\n <a\n target=\"_blank\"\n href={releaseLink}\n className=\"newVersion\"\n >\n Version <code className=\"version\">{latestVersion}</code> is available\n </a>\n }\n </div>;\n}\n\ntype UpdateBadgeProps = {\n appVersion?: string,\n canShowCurrentVersion?: boolean\n};\n\nasync function getLatestAvailableVersion(includePrerelease: boolean = false): Promise<{\n version?: string,\n url: string\n}> {\n try {\n if (includePrerelease) {\n const latestReleases = await getLatestPrereleaseAndRelease();\n if (latestReleases.latestPrerelease != null && latestReleases.latestRelease != null) {\n if (semver.gt(latestReleases.latestPrerelease.version, latestReleases.latestRelease.version))\n return {\n version: latestReleases.latestPrerelease.version,\n url: latestReleases.latestPrerelease.url\n };\n\n return {\n version: latestReleases.latestRelease.version,\n url: latestReleaseUrl\n };\n } else if (latestReleases.latestPrerelease != null) {\n return {\n version: latestReleases.latestPrerelease.version,\n url: latestReleases.latestPrerelease.url\n };\n } else if (latestReleases.latestRelease != null) {\n return {\n version: latestReleases.latestRelease.version,\n url: latestReleaseUrl\n };\n }\n }\n\n const releaseRes = await fetch(\"https://api.github.com/repos/withcatai/node-llama-cpp/releases/latest\");\n const release: {\n tag_name: string\n } = await releaseRes.json();\n\n return {\n version: normalizeTagName(release?.tag_name),\n url: latestReleaseUrl\n };\n } catch (err) {\n console.error(err);\n return {\n version: undefined,\n url: latestReleaseUrl\n };\n }\n}\n\nasync function getLatestPrereleaseAndRelease(): Promise<{\n latestRelease?: {\n version: string,\n url: string\n },\n latestPrerelease?: {\n version: string,\n url: string\n }\n}> {\n try {\n const releasesRes = await fetch(\"https://api.github.com/repos/withcatai/node-llama-cpp/releases?per_page=100\");\n const releases: Array<{\n tag_name: string,\n html_url: string,\n prerelease: boolean,\n draft: boolean\n }> = await releasesRes.json();\n\n const latestRelease = releases.find((release) => !release.prerelease && !release.draft);\n const latestPrerelease = releases.find((release) => release.prerelease && !release.draft);\n\n return {\n latestRelease: latestRelease == null ? undefined : {\n version: normalizeTagName(latestRelease.tag_name)!,\n url: latestRelease.html_url\n },\n latestPrerelease: latestPrerelease == null ? undefined : {\n version: normalizeTagName(latestPrerelease.tag_name)!,\n url: latestPrerelease.html_url\n }\n };\n } catch (err) {\n console.error(err);\n return {};\n }\n}\n\nfunction normalizeTagName(tagName?: string) {\n if (tagName == null)\n return undefined;\n\n if (tagName.toLowerCase().startsWith(\"v\"))\n return tagName.slice(\"v\".length);\n\n return tagName;\n}\n"},{"path":["src","App","components","InputRow","InputRow.css"],"content":".appInputRow {\n display: flex;\n flex-direction: row;\n bottom: 16px;\n flex-shrink: 0;\n align-items: flex-end;\n\n &.spacer {\n position: sticky;\n pointer-events: none;\n }\n\n &.main {\n width: calc(100% - 16px * 2);\n max-width: var(--app-max-width, 1280px);\n position: fixed;\n background-color: var(--panel-background-color);\n border-radius: 12px;\n backdrop-filter: blur(8px);\n box-shadow: var(--panel-box-shadow);\n overflow: clip;\n color: var(--panel-text-color);\n z-index: 10;\n\n &.disabled {\n opacity: 0.48;\n }\n\n > .inputContainer {\n flex: 1;\n display: flex;\n flex-direction: row;\n overflow: hidden;\n position: relative;\n isolation: isolate;\n max-height: 400px;\n min-height: var(--min-height);\n --min-height: 55px;\n\n > .input {\n flex: 1;\n border: none;\n resize: none;\n box-sizing: border-box;\n max-height: 160px;\n min-height: var(--min-height);\n height: 55px;\n outline: none;\n padding: calc((var(--min-height) - 1lh) / 2) 24px;\n background-color: transparent;\n font: inherit;\n align-self: stretch;\n color: var(--panel-text-color);\n z-index: 2;\n unicode-bidi: plaintext;\n overflow: auto;\n\n &::placeholder {\n color: var(--panel-text-color);\n opacity: 0.4;\n }\n }\n\n > .autocomplete {\n position: absolute;\n inset: 0px;\n z-index: 1;\n display: flex;\n overflow: hidden;\n pointer-events: none;\n user-select: none;\n\n > .content {\n flex: 1;\n flex-shrink: 0;\n font: inherit;\n padding: calc((var(--min-height) - 1lh) / 2) 24px;\n text-align: initial;\n unicode-bidi: plaintext;\n overflow: hidden;\n opacity: 0.36;\n mask: linear-gradient(to top, rgb(0 0 0 / 16%), black 24px);\n\n &.hide {\n opacity: 0;\n }\n\n > .currentText {\n opacity: 0;\n display: inline;\n white-space: pre-wrap;\n word-break: break-word;\n unicode-bidi: normal;\n }\n\n > .completion {\n display: inline;\n white-space: pre-wrap;\n word-break: break-word;\n unicode-bidi: normal;\n }\n\n > .pressTab {\n display: inline-block;\n margin: -1px 8px;\n opacity: 0.8;\n border: solid 1px color-mix(in srgb, currentColor, transparent 64%);\n border-bottom-width: 2px;\n border-radius: 8px;\n padding: 0.1em 0.4em;\n font-size: 0.8em;\n vertical-align: top;\n }\n }\n }\n }\n\n > .stopGenerationButton,\n > .sendButton {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 8px 12px;\n margin: 8px;\n background-color: var(--panel-button-background-color);\n color: var(--panel-text-color);\n fill: var(--panel-text-color);\n\n + button {\n margin-inline-start: 0px;\n }\n\n &:hover,\n &:focus,\n &:focus-visible {\n border-color: var(--panel-button-hover-border-color);\n }\n\n > .icon {\n width: 20px;\n height: 20px;\n }\n }\n\n > .stopGenerationButton {\n transition: border-color 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing);\n\n &[disabled] {\n opacity: 0;\n }\n }\n }\n}\n"},{"path":["src","App","components","InputRow","InputRow.tsx"],"content":"import {useCallback, useMemo, useRef, useState} from \"react\";\nimport classNames from \"classnames\";\nimport {AddMessageIconSVG} from \"../../../icons/AddMessageIconSVG.tsx\";\nimport {AbortIconSVG} from \"../../../icons/AbortIconSVG.tsx\";\nimport {FixedDivWithSpacer} from \"../FixedDivWithSpacer/FixedDivWithSpacer.tsx\";\n\nimport \"./InputRow.css\";\n\n\nexport function InputRow({\n disabled = false, stopGeneration, sendPrompt, onPromptInput, autocompleteInputDraft, autocompleteCompletion, generatingResult\n}: InputRowProps) {\n const [inputText, setInputText] = useState<string>(\"\");\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const autocompleteRef = useRef<HTMLDivElement>(null);\n const autocompleteCurrentTextRef = useRef<HTMLDivElement>(null);\n\n const autocompleteText = useMemo(() => {\n const fullText = (autocompleteInputDraft ?? \"\") + (autocompleteCompletion ?? \"\");\n if (fullText.startsWith(inputText))\n return fullText.slice(inputText.length);\n\n return \"\";\n }, [inputText, autocompleteInputDraft, autocompleteCompletion]);\n\n const setInputValue = useCallback((value: string) => {\n if (inputRef.current != null)\n inputRef.current.value = value;\n\n if (autocompleteCurrentTextRef.current != null)\n autocompleteCurrentTextRef.current.innerText = value;\n\n setInputText(value);\n }, []);\n\n const resizeInput = useCallback(() => {\n if (inputRef.current == null)\n return;\n\n inputRef.current.style.height = \"\";\n inputRef.current.style.height = inputRef.current.scrollHeight + \"px\";\n\n if (autocompleteRef.current != null) {\n autocompleteRef.current.scrollTop = inputRef.current.scrollTop;\n }\n }, []);\n\n const submitPrompt = useCallback(() => {\n if (generatingResult || inputRef.current == null)\n return;\n\n const message = inputRef.current.value;\n if (message.length === 0)\n return;\n\n setInputValue(\"\");\n resizeInput();\n onPromptInput?.(\"\");\n sendPrompt(message);\n }, [setInputValue, generatingResult, resizeInput, sendPrompt, onPromptInput]);\n\n const onInput = useCallback(() => {\n setInputText(inputRef.current?.value ?? \"\");\n resizeInput();\n\n if (autocompleteCurrentTextRef.current != null && inputRef.current != null)\n autocompleteCurrentTextRef.current.innerText = inputRef.current?.value;\n\n if (inputRef.current != null && onPromptInput != null)\n onPromptInput(inputRef.current?.value);\n }, [resizeInput, onPromptInput]);\n\n const onInputKeyDown = useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n submitPrompt();\n } else if (event.key === \"Tab\" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {\n event.preventDefault();\n if (inputRef.current != null && autocompleteText !== \"\") {\n const newlineIndex = autocompleteText.indexOf(\"\\n\");\n const textToAccept = newlineIndex <= 0\n ? autocompleteText\n : autocompleteText.slice(0, newlineIndex);\n\n setInputValue(inputRef.current.value + textToAccept);\n inputRef.current.scrollTop = inputRef.current.scrollHeight;\n onPromptInput?.(inputRef.current.value);\n }\n\n resizeInput();\n }\n }, [submitPrompt, setInputValue, onPromptInput, resizeInput, autocompleteText]);\n\n const previewAutocompleteText = useMemo(() => {\n const lines = autocompleteText.split(\"\\n\");\n if (lines.length <= 1 || lines[1]!.trim() === \"\")\n return lines[0]!;\n\n return autocompleteText;\n }, [autocompleteText]);\n\n // we use a FixedDivWithSpacer to push down the content while keeping the input fixed.\n // this allows the content to have macOS's scroll bounce while keeping the input fixed at the bottom.\n return <FixedDivWithSpacer className={classNames(\"appInputRow\", disabled && \"disabled\")}>\n <div className=\"inputContainer\">\n <textarea\n ref={inputRef}\n onInput={onInput}\n onKeyDownCapture={onInputKeyDown}\n className=\"input\"\n autoComplete=\"off\"\n spellCheck\n disabled={disabled}\n onScroll={resizeInput}\n placeholder={\n autocompleteText === \"\"\n ? \"Type a message...\"\n : \"\"\n }\n />\n <div className=\"autocomplete\" ref={autocompleteRef}>\n <div className={classNames(\"content\", autocompleteText === \"\" && \"hide\")}>\n <div className=\"currentText\" ref={autocompleteCurrentTextRef} />\n <div className=\"completion\">{previewAutocompleteText}</div>\n <div className=\"pressTab\">Tab</div>\n </div>\n </div>\n </div>\n <button\n className=\"stopGenerationButton\"\n disabled={disabled || stopGeneration == null || !generatingResult}\n onClick={stopGeneration}\n >\n <AbortIconSVG className=\"icon\" />\n </button>\n <button\n className=\"sendButton\"\n disabled={disabled || inputText === \"\" || generatingResult}\n onClick={submitPrompt}\n >\n <AddMessageIconSVG className=\"icon\" />\n </button>\n </FixedDivWithSpacer>;\n}\n\ntype InputRowProps = {\n disabled?: boolean,\n stopGeneration?(): void,\n sendPrompt(prompt: string): void,\n onPromptInput?(currentText: string): void,\n autocompleteInputDraft?: string,\n autocompleteCompletion?: string,\n generatingResult: boolean\n};\n"},{"path":["src","App","components","MarkdownContent","MarkdownContent.css"],"content":"/* Only style code blocks */\npre > code {\n display: block;\n background-color: var(--code-block-background-color);\n padding: 0.8em 1.2em;\n border-radius: 0.4em;\n font-size: 1.2em;\n overflow-x: auto;\n user-select: all;\n\n @media (prefers-color-scheme: light) {\n /*!\n Theme: GitHub\n Description: Light theme as seen on github.com\n Author: github.com\n Maintainer: @Hirse\n Updated: 2021-05-15\n\n Outdated base version: https://github.com/primer/github-syntax-light\n Current colors taken from GitHub's CSS\n */\n .hljs {\n color: #24292e;\n background: #ffffff\n }\n .hljs-doctag,\n .hljs-keyword,\n .hljs-meta .hljs-keyword,\n .hljs-template-tag,\n .hljs-template-variable,\n .hljs-type,\n .hljs-variable.language_ {\n /* prettylights-syntax-keyword */\n color: #d73a49\n }\n .hljs-title,\n .hljs-title.class_,\n .hljs-title.class_.inherited__,\n .hljs-title.function_ {\n /* prettylights-syntax-entity */\n color: #6f42c1\n }\n .hljs-attr,\n .hljs-attribute,\n .hljs-literal,\n .hljs-meta,\n .hljs-number,\n .hljs-operator,\n .hljs-variable,\n .hljs-selector-attr,\n .hljs-selector-class,\n .hljs-selector-id {\n /* prettylights-syntax-constant */\n color: #005cc5\n }\n .hljs-regexp,\n .hljs-string,\n .hljs-meta .hljs-string {\n /* prettylights-syntax-string */\n color: #032f62\n }\n .hljs-built_in,\n .hljs-symbol {\n /* prettylights-syntax-variable */\n color: #e36209\n }\n .hljs-comment,\n .hljs-code,\n .hljs-formula {\n /* prettylights-syntax-comment */\n color: #6a737d\n }\n .hljs-name,\n .hljs-quote,\n .hljs-selector-tag,\n .hljs-selector-pseudo {\n /* prettylights-syntax-entity-tag */\n color: #22863a\n }\n .hljs-subst {\n /* prettylights-syntax-storage-modifier-import */\n color: #24292e\n }\n .hljs-section {\n /* prettylights-syntax-markup-heading */\n color: #005cc5;\n font-weight: bold\n }\n .hljs-bullet {\n /* prettylights-syntax-markup-list */\n color: #735c0f\n }\n .hljs-emphasis {\n /* prettylights-syntax-markup-italic */\n color: #24292e;\n font-style: italic\n }\n .hljs-strong {\n /* prettylights-syntax-markup-bold */\n color: #24292e;\n font-weight: bold\n }\n .hljs-addition {\n /* prettylights-syntax-markup-inserted */\n color: #22863a;\n background-color: #f0fff4\n }\n .hljs-deletion {\n /* prettylights-syntax-markup-deleted */\n color: #b31d28;\n background-color: #ffeef0\n }\n .hljs-char.escape_,\n .hljs-link,\n .hljs-params,\n .hljs-property,\n .hljs-punctuation,\n .hljs-tag {\n /* purposely ignored */\n }\n }\n\n @media (prefers-color-scheme: dark) {\n /*!\n Theme: GitHub Dark\n Description: Dark theme as seen on github.com\n Author: github.com\n Maintainer: @Hirse\n Updated: 2021-05-15\n\n Outdated base version: https://github.com/primer/github-syntax-dark\n Current colors taken from GitHub's CSS\n */\n .hljs {\n color: #c9d1d9;\n background: #0d1117\n }\n .hljs-doctag,\n .hljs-keyword,\n .hljs-meta .hljs-keyword,\n .hljs-template-tag,\n .hljs-template-variable,\n .hljs-type,\n .hljs-variable.language_ {\n /* prettylights-syntax-keyword */\n color: #ff7b72\n }\n .hljs-title,\n .hljs-title.class_,\n .hljs-title.class_.inherited__,\n .hljs-title.function_ {\n /* prettylights-syntax-entity */\n color: #d2a8ff\n }\n .hljs-attr,\n .hljs-attribute,\n .hljs-literal,\n .hljs-meta,\n .hljs-number,\n .hljs-operator,\n .hljs-variable,\n .hljs-selector-attr,\n .hljs-selector-class,\n .hljs-selector-id {\n /* prettylights-syntax-constant */\n color: #79c0ff\n }\n .hljs-regexp,\n .hljs-string,\n .hljs-meta .hljs-string {\n /* prettylights-syntax-string */\n color: #a5d6ff\n }\n .hljs-built_in,\n .hljs-symbol {\n /* prettylights-syntax-variable */\n color: #ffa657\n }\n .hljs-comment,\n .hljs-code,\n .hljs-formula {\n /* prettylights-syntax-comment */\n color: #8b949e\n }\n .hljs-name,\n .hljs-quote,\n .hljs-selector-tag,\n .hljs-selector-pseudo {\n /* prettylights-syntax-entity-tag */\n color: #7ee787\n }\n .hljs-subst {\n /* prettylights-syntax-storage-modifier-import */\n color: #c9d1d9\n }\n .hljs-section {\n /* prettylights-syntax-markup-heading */\n color: #1f6feb;\n font-weight: bold\n }\n .hljs-bullet {\n /* prettylights-syntax-markup-list */\n color: #f2cc60\n }\n .hljs-emphasis {\n /* prettylights-syntax-markup-italic */\n color: #c9d1d9;\n font-style: italic\n }\n .hljs-strong {\n /* prettylights-syntax-markup-bold */\n color: #c9d1d9;\n font-weight: bold\n }\n .hljs-addition {\n /* prettylights-syntax-markup-inserted */\n color: #aff5b4;\n background-color: #033a16\n }\n .hljs-deletion {\n /* prettylights-syntax-markup-deleted */\n color: #ffdcd7;\n background-color: #67060c\n }\n .hljs-char.escape_,\n .hljs-link,\n .hljs-params,\n .hljs-property,\n .hljs-punctuation,\n .hljs-tag {\n /* purposely ignored */\n }\n }\n}\n"},{"path":["src","App","components","MarkdownContent","MarkdownContent.tsx"],"content":"import {useLayoutEffect, useRef} from \"react\";\nimport markdownit from \"markdown-it\";\nimport hljs from \"highlight.js\";\n\nimport \"./MarkdownContent.css\";\n\nconst md = markdownit({\n highlight(str, lang): string {\n if (hljs.getLanguage(lang) != null) {\n try {\n return hljs.highlight(str, {language: lang}).value;\n } catch (err) {\n // do nothing\n }\n }\n\n return hljs.highlightAuto(str).value;\n }\n});\n\nexport function MarkdownContent({children, inline = false, dir, className}: MarkdownContentProps) {\n const divRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n if (divRef.current == null)\n return;\n\n if (inline)\n divRef.current.innerHTML = md.renderInline(children ?? \"\").replaceAll(\"<br>\", \"\");\n else\n divRef.current.innerHTML = md.render(children ?? \"\");\n }, [inline, children]);\n\n return <div\n className={className}\n ref={divRef}\n dir={dir}\n />;\n}\n\ntype MarkdownContentProps = {\n className?: string,\n inline?: boolean,\n dir?: string,\n children: string\n};\n"},{"path":["src","App","components","MessageMarkdown","MessageMarkdown.css"],"content":".appMessageMarkdown {\n word-break: break-word;\n\n &.active {\n &:empty:after,\n &:not(:empty)>:last-child:not(ol, ul, table):after,\n &:not(:empty)>:last-child:where(ol, ul)>:last-child:not(:has(>:last-child:where(ol, ul))):after,\n &:not(:empty)>:last-child:where(ol, ul)>:last-child>:last-child:where(ol, ul)>:last-child:after,\n &:not(:empty)>:last-child:where(table)>:last-child>:last-child>:last-child:after {\n content: \"\";\n position: static;\n display: inline-block;\n background-color: currentColor;\n width: 8px;\n height: 8px;\n translate: 0px -2px;\n border-radius: 9999px;\n margin-inline-start: 8px;\n vertical-align: middle;\n\n animation: messageMarkdownActiveDot 2s infinite ease-in-out;\n }\n }\n\n > :first-child {\n margin-top: 0px;\n }\n\n > :last-child {\n margin-bottom: 0px;\n }\n\n * {\n unicode-bidi: plaintext;\n }\n\n h2 {\n margin: 16px 0px;\n padding-top: 24px;\n }\n\n h3 {\n margin: 32px 0px 0px 0px;\n }\n\n hr {\n background-color: var(--message-hr-color);\n height: 2px;\n border: none;\n border-radius: 12px;\n }\n\n blockquote {\n margin: 0px 0px;\n padding-inline-start: 24px;\n opacity: 0.64;\n border: none;\n position: relative;\n\n &:before {\n content: \"\";\n position: absolute;\n width: 4px;\n height: 100%;\n background-color: var(--message-blockquote-border-color);\n inset-inline-start: 0px;\n }\n }\n\n table {\n display: block;\n border-style: hidden;\n border-radius: 12px;\n outline: solid 1px var(--message-table-outline-color);\n outline-offset: -1px;\n max-width: max-content;\n border-collapse: collapse;\n overflow-x: auto;\n background-color: var(--background-color);\n\n thead {\n text-align: justify;\n }\n\n tr {\n background-color: var(--message-table-background-color);\n border-top: 1px solid var(--message-table-outline-color);\n\n &:nth-child(2n) td {\n background-color: var(--message-table-even-background-color);\n }\n\n th {\n background-color: var(--message-table-even-background-color);\n border: 1px solid var(--message-table-outline-color);\n padding: 8px 16px;\n }\n\n td {\n border: 1px solid var(--message-table-outline-color);\n padding: 8px 16px;\n }\n }\n }\n}\n\n@keyframes messageMarkdownActiveDot {\n 0% {\n transform: scale(1);\n opacity: 0.64;\n }\n 50% {\n transform: scale(1.4);\n opacity: 0.32;\n }\n 100% {\n transform: scale(1);\n opacity: 0.64;\n }\n}\n"},{"path":["src","App","components","MessageMarkdown","MessageMarkdown.tsx"],"content":"import {useMemo} from \"react\";\nimport classNames from \"classnames\";\nimport {MarkdownContent} from \"../MarkdownContent/MarkdownContent.js\";\n\nimport \"./MessageMarkdown.css\";\n\n\nexport function MessageMarkdown({children, activeDot = false, className}: MessageMarkdownProps) {\n const renderContent = useMemo(() => {\n if (children == null)\n return \"\";\n\n if (!activeDot)\n return children;\n\n const lines = children.split(\"\\n\");\n const lastLine = lines.at(-1);\n\n // to frequent line jumps and instability while the content is being generated,\n // wait with rendering the last line until its content is properly formed and is ready to be appended\n if (lastLine != null && [\"-\", \"+\", \"*\", \"1.\", \"1\", \"--\"].includes(lastLine.trim()))\n return lines.slice(0, -1).join(\"\\n\");\n else if (lastLine != null && lastLine.trim().length === 1 && (\n lastLine.endsWith(\" *\") || lastLine.endsWith(\" _\") || lastLine.endsWith(\" ~\")\n ))\n return [\n ...lines.slice(0, -1),\n lastLine.slice(0, -\" _\".length)\n ].join(\"\\n\");\n\n return children;\n }, [children, activeDot]);\n\n return <MarkdownContent className={classNames(\"appMessageMarkdown\", activeDot && \"active\", className)}>\n {renderContent}\n </MarkdownContent>;\n}\n\ntype MessageMarkdownProps = {\n children?: string,\n activeDot?: boolean,\n className?: string\n};\n"},{"path":["src","hooks","useExternalState.ts"],"content":"import {useEffect, useState} from \"react\";\nimport {State} from \"lifecycle-utils\";\n\nexport function useExternalState<const StateType, const R>(state: State<StateType>, selector: ((state: StateType) => R)): R;\nexport function useExternalState<const StateType>(state: State<StateType>): StateType;\nexport function useExternalState<const StateType>(state: State<StateType>, selector?: ((state: StateType) => any) | null): StateType {\n const [currentState, setCurrentState] = useState(() => (\n selector == null\n ? state.state\n : selector(state.state)\n ));\n\n useEffect(() => {\n return state.createChangeListener((newState) => {\n setCurrentState(\n selector == null\n ? newState\n : selector(newState)\n );\n }, true).dispose;\n }, [state]);\n\n return currentState;\n}\n"},{"path":["src","icons","AbortIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function AbortIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M360-320h240q17 0 28.5-11.5T640-360v-240q0-17-11.5-28.5T600-640H360q-17 0-28.5 11.5T320-600v240q0 17 11.5 28.5T360-320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z\" />\n </svg>;\n}\n"},{"path":["src","icons","AddMessageIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function AddMessageIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M440-647 244-451q-12 12-28 11.5T188-452q-11-12-11.5-28t11.5-28l264-264q6-6 13-8.5t15-2.5q8 0 15 2.5t13 8.5l264 264q11 11 11 27.5T772-452q-12 12-28.5 12T715-452L520-647v447q0 17-11.5 28.5T480-160q-17 0-28.5-11.5T440-200v-447Z\" />\n </svg>;\n}\n"},{"path":["src","icons","CheckIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function CheckIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z\" />\n </svg>;\n}\n"},{"path":["src","icons","CopyIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function CopyIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z\" />\n </svg>;\n}\n"},{"path":["src","icons","DeleteIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function DeleteIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M280-120q-33 0-56.5-23.5T200-200v-520q-17 0-28.5-11.5T160-760q0-17 11.5-28.5T200-800h160q0-17 11.5-28.5T400-840h160q17 0 28.5 11.5T600-800h160q17 0 28.5 11.5T800-760q0 17-11.5 28.5T760-720v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520Zm-400 0v520-520Zm200 316 76 76q11 11 28 11t28-11q11-11 11-28t-11-28l-76-76 76-76q11-11 11-28t-11-28q-11-11-28-11t-28 11l-76 76-76-76q-11-11-28-11t-28 11q-11 11-11 28t11 28l76 76-76 76q-11 11-11 28t11 28q11 11 28 11t28-11l76-76Z\" />\n </svg>;\n}\n"},{"path":["src","icons","DownloadIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function DownloadIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z\" />\n </svg>;\n}\n"},{"path":["src","icons","LoadFileIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function LoadFileIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M440-367v127q0 17 11.5 28.5T480-200q17 0 28.5-11.5T520-240v-127l36 36q6 6 13.5 9t15 2.5q7.5-.5 14.5-3.5t13-9q11-12 11.5-28T612-388L508-492q-6-6-13-8.5t-15-2.5q-8 0-15 2.5t-13 8.5L348-388q-12 12-11.5 28t12.5 28q12 11 28 11.5t28-11.5l35-35ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h287q16 0 30.5 6t25.5 17l194 194q11 11 17 25.5t6 30.5v447q0 33-23.5 56.5T720-80H240Zm280-560v-160H240v640h480v-440H560q-17 0-28.5-11.5T520-640ZM240-800v200-200 640-640Z\" />\n </svg>;\n}\n"},{"path":["src","icons","RightChevronIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function RightChevronIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M522-480 358-644q-11-11-11-25.5t11-25.5q11-11 25.5-11t25.84 11.34L599-505q5 5.4 7.5 11.7 2.5 6.3 2.5 13.5t-2.5 13.5Q604-460 599-455L409.34-265.34Q398-254 384-254.5T359-266q-11-11-11-25.5t11-25.5l163-163Z\" />\n </svg>;\n}\n"},{"path":["src","icons","SearchIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function SearchIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"M380-320q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z\" />\n </svg>;\n}\n"},{"path":["src","icons","StarIconSVG.tsx"],"content":"import {SVGProps} from \"react\";\n\nexport function StarIconSVG(props: SVGProps<SVGSVGElement>) {\n return <svg height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" {...props}>\n <path d=\"m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143Zm126 18L314-169q-11 7-23 6t-21-8q-9-7-14-17.5t-2-23.5l44-189-147-127q-10-9-12.5-20.5T140-571q4-11 12-18t22-9l194-17 75-178q5-12 15.5-18t21.5-6q11 0 21.5 6t15.5 18l75 178 194 17q14 2 22 9t12 18q4 11 1.5 22.5T809-528L662-401l44 189q3 13-2 23.5T690-171q-9 7-21 8t-23-6L480-269Zm0-201Z\" />\n </svg>;\n}\n"},{"path":["src","index.css"],"content":":root {\n font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n line-height: 1.5;\n font-weight: 400;\n\n color-scheme: light dark;\n background-color: var(--background-color);\n color: var(--text-color);\n\n font-synthesis: none;\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n\n --transition-easing: cubic-bezier(0.4, 0.0, 0.2, 1);\n}\n\n:root {\n --background-color: light-dark(#eeeeee, #242424);\n --text-color: light-dark(#242424, #eeeeee);\n\n --button-background-color: light-dark(rgba(255 255 255 / 64%), rgba(255 255 255 / 8%));\n --button-hover-border-color: light-dark(#646cff, #646cff);\n\n --link-color: light-dark(#646cff, #646cff);\n --link-hover-color: light-dark(#747bff, #535bf2);\n --star-link-color: light-dark(#e09c1c, #eac54f);\n --star-hover-color: light-dark(#daaa52, #c7a027);\n\n --panel-background-color: light-dark(rgba(0 0 0 / 52%), rgba(0 0 0 / 48%));\n --panel-text-color: light-dark(#ffffff, #eeeeee);\n --panel-button-background-color: light-dark(rgba(0 0 0 / 24%), rgba(255 255 255 / 8%));\n --panel-button-hover-border-color: light-dark(var(--panel-text-color), var(--button-hover-border-color));\n --panel-progress-color: light-dark(rgba(255 255 255 / 8%), rgba(255 255 255 / 4%));\n --panel-box-shadow: 0px 4px 8px 0px rgba(0 0 0 / 8%), 0px 6px 24px 0px rgba(0 0 0 / 16%);\n\n --error-border-color: light-dark(rgba(239 83 80 / 100%), rgba(239 83 80 / 100%));\n\n --user-message-background-color: light-dark(rgba(0 0 0 / 16%), rgba(0 0 0 / 48%));\n --user-message-text-color: light-dark(#242424, var(--text-color));\n\n --actions-block-background-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --actions-block-border-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --actions-block-box-shadow: 0px 4px 8px 0px rgba(0 0 0 / 2%), 0px 6px 24px 0px rgba(0 0 0 / 8%);\n\n --code-block-background-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n\n --update-badge-background-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --update-badge-border-color: light-dark(rgba(0 0 0 / 4%), rgba(0 0 0 / 16%));\n --update-badge-box-shadow: 0px 4px 8px 0px rgba(0 0 0 / 2%), 0px 6px 24px 0px rgba(0 0 0 / 8%);\n\n --message-table-outline-color: light-dark(rgba(0 0 0 / 8%), rgba(255 255 255 / 6%));\n --message-table-background-color: light-dark(rgba(0 0 0 / 0%), rgba(0 0 0 / 12%));\n --message-table-even-background-color: light-dark(rgba(0 0 0 / 4%), rgba(255 255 255 / 6%));\n\n --message-hr-color: light-dark(rgba(0 0 0 / 16%), rgba(255 255 255 / 16%));\n --message-blockquote-border-color: light-dark(rgba(0 0 0 / 8%), rgba(255 255 255 / 8%));\n\n --model-comment-block-background-color: light-dark(rgba(0 0 0 / 6%), rgba(0 0 0 / 24%));\n --model-comment-block-button-background-color: light-dark(rgba(0 0 0 / 6%), rgba(0 0 0 / 24%));\n}\n\nbody {\n margin: 0;\n display: flex;\n min-width: 320px;\n min-height: 100vh;\n overscroll-behavior-x: none;\n}\n\na {\n font-weight: 500;\n color: var(--link-color);\n text-decoration: inherit;\n\n &:hover {\n color: var(--link-hover-color);\n }\n}\n\ncode {\n background-color: color-mix(in srgb, currentColor, transparent 84%);\n padding: 0.1em 0.3em;\n border-radius: 0.4em;\n font-size: 1.2em;\n}\n\nbutton {\n border-radius: 8px;\n border: 1px solid transparent;\n padding: 0.6em 1.2em;\n font-size: 1em;\n font-weight: 500;\n font-family: inherit;\n background-color: var(--button-background-color);\n cursor: pointer;\n transition: border-color 0.3s var(--transition-easing), opacity 0.3s var(--transition-easing);\n color: var(--text-color);\n fill: var(--text-color);\n\n &[disabled] {\n opacity: 0.52;\n pointer-events: none;\n }\n\n &:hover,\n &:focus,\n &:focus-visible {\n border-color: var(--button-hover-border-color);\n }\n}\n\n"},{"path":["src","index.html"],"content":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\"/>\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\"/>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n <title>Electron + TypeScript + React + Vite + node-llama-cpp</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"./index.tsx\"></script>\n </body>\n</html>\n"},{"path":["src","index.tsx"],"content":"import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport \"@fontsource-variable/inter/opsz-italic.css\";\nimport {App} from \"./App/App.tsx\";\nimport \"./index.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>\n);\n\n// Use contextBridge\nwindow.ipcRenderer.on(\"main-process-message\", (_event, message) => {\n console.log(message);\n});\n"},{"path":["src","rpc","llmRpc.ts"],"content":"import {ElectronFunctions} from \"../../electron/rpc/llmRpc.ts\";\nimport {createRendererSideBirpc} from \"../utils/createRendererSideBirpc.ts\";\nimport {llmState} from \"../state/llmState.ts\";\nimport {LlmState} from \"../../electron/state/llmState.ts\";\n\n\nconst renderedFunctions = {\n updateState(state: LlmState) {\n llmState.state = state;\n }\n} as const;\nexport type RenderedFunctions = typeof renderedFunctions;\n\nexport const electronLlmRpc = createRendererSideBirpc<ElectronFunctions, RenderedFunctions>(\"llmRpc\", \"llmRpc\", renderedFunctions);\n\nelectronLlmRpc.getState()\n .then((state) => {\n llmState.state = state;\n })\n .catch((error) => {\n console.error(\"Failed to get the initial state from the main process\", error);\n });\n"},{"path":["src","state","llmState.ts"],"content":"import {State} from \"lifecycle-utils\";\n\nimport {LlmState} from \"../../electron/state/llmState.ts\";\n\nexport const llmState = new State<LlmState>({\n appVersion: undefined,\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"},{"path":["src","utils","createRendererSideBirpc.ts"],"content":"import {createBirpc} from \"birpc\";\n\nexport function createRendererSideBirpc<\n const ElectronFunction extends object = Record<string, never>,\n const RendererFunctions extends object = Record<string, never>\n>(\n toRendererEventName: string,\n fromRendererEventName: string,\n rendererFunctions: RendererFunctions\n) {\n return createBirpc<ElectronFunction, RendererFunctions>(rendererFunctions, {\n post: (data) => window.ipcRenderer.send(fromRendererEventName, data),\n on: (onData) => window.ipcRenderer.on(toRendererEventName, (event, data) => {\n onData(data);\n }),\n serialize: (value) => JSON.stringify(value),\n deserialize: (value) => JSON.parse(value)\n });\n}\n"},{"path":["src","vite-env.d.ts"],"content":"/// <reference types=\"vite/client\" />\n"},{"path":["tsconfig.json"],"content":"{\n \"compilerOptions\": {\n \"target\": \"es2022\",\n \"lib\": [\"es2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"es2022\",\n \"skipLibCheck\": true,\n\n \"esModuleInterop\": true,\n \"removeComments\": false,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"noUncheckedIndexedAccess\": true,\n \"moduleDetection\": \"force\",\n\n \"moduleResolution\": \"bundler\",\n \"allowImportingTsExtensions\": true,\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n\n \"strict\": true,\n \"strictNullChecks\": true,\n \"noImplicitAny\": true,\n \"noImplicitReturns\": true,\n \"noImplicitThis\": true,\n \"noImplicitOverride\": true\n },\n \"include\": [\n \"./src\",\n \"./electron\"\n ],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"},{"path":["tsconfig.node.json"],"content":"{\n \"compilerOptions\": {\n \"target\": \"es2022\",\n \"lib\": [\"es2022\"],\n \"module\": \"es2022\",\n \"skipLibCheck\": true,\n\n \"esModuleInterop\": true,\n \"removeComments\": false,\n \"allowSyntheticDefaultImports\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"noUncheckedIndexedAccess\": true,\n \"moduleDetection\": \"force\",\n\n \"moduleResolution\": \"bundler\",\n \"resolveJsonModule\": false,\n \"isolatedModules\": true,\n \"composite\": true,\n\n \"strict\": true,\n \"strictNullChecks\": true,\n \"noImplicitAny\": true,\n \"noImplicitReturns\": true,\n \"noImplicitThis\": true,\n \"noImplicitOverride\": true\n },\n \"include\": [\n \"vite.config.ts\"\n ]\n}\n"},{"path":["vite.config.ts"],"content":"import path from \"node:path\";\nimport {fileURLToPath} from \"node:url\";\nimport {defineConfig} from \"vite\";\nimport electron from \"vite-plugin-electron/simple\";\nimport react from \"@vitejs/plugin-react\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// These modules won't be bundled as part of the Vite build of the Electron (main) side,\n// but they'll be included in the final Electron app build inside the asar file.\n// Performance and efficiency wise, this is absolutely fine and has no real drawbacks\nconst electronExternalModules = [\"node-llama-cpp\", \"lifecycle-utils\"];\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n esbuild: {\n target: \"es2022\"\n },\n optimizeDeps: {\n exclude: electronExternalModules,\n esbuildOptions: {\n target: \"es2022\"\n }\n },\n build: {\n outDir: path.join(__dirname, \"dist\"),\n target: \"es2022\"\n },\n root: path.join(__dirname, \"src\"),\n publicDir: path.join(__dirname, \"public\"),\n plugins: [\n react(),\n electron({\n main: {\n // Shortcut of `build.lib.entry`.\n entry: path.join(__dirname, \"electron/index.ts\"),\n onstart({startup}) {\n if (process.env[\"ENABLE_INSPECT\"] === \"true\")\n return startup([\".\", \"--inspect\"]);\n\n return startup([\".\"]);\n },\n vite: {\n build: {\n target: \"es2022\",\n outDir: path.join(__dirname, \"dist-electron\"),\n rollupOptions: {\n external: electronExternalModules\n }\n }\n }\n },\n preload: {\n // Shortcut of `build.rollupOptions.input`.\n // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.\n input: path.join(__dirname, \"electron/preload.ts\"),\n vite: {\n build: {\n target: \"es2022\",\n outDir: path.join(__dirname, \"dist-electron\")\n }\n }\n },\n // Polyfill the Electron and Node.js API for Renderer process.\n // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process.\n // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer\n renderer: process.env.NODE_ENV === \"test\"\n // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808\n ? undefined\n : {}\n })\n ]\n});\n"}]}