{"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} satisfies Configuration 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\": \"node16\",\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\": \"node16\",\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"}]}