├── .dockerignore
├── .env.example
├── .github
└── FUNDING.yml
├── .gitignore
├── .vscode
└── extensions.json
├── Dockerfile
├── LICENSE
├── README.md
├── bun.lock
├── eslint.config.mjs
├── index.html
├── nginx.conf
├── package.json
├── public
├── assets
│ └── fonts
│ │ ├── NebulaSans-Black.woff2
│ │ ├── NebulaSans-BlackItalic.woff2
│ │ ├── NebulaSans-Bold.woff2
│ │ ├── NebulaSans-BoldItalic.woff2
│ │ ├── NebulaSans-Book.woff2
│ │ ├── NebulaSans-BookItalic.woff2
│ │ ├── NebulaSans-Light.woff2
│ │ ├── NebulaSans-LightItalic.woff2
│ │ ├── NebulaSans-Medium.woff2
│ │ ├── NebulaSans-MediumItalic.woff2
│ │ ├── NebulaSans-Semibold.woff2
│ │ └── NebulaSans-SemiboldItalic.woff2
├── favicon.svg
├── icons
│ ├── logo-120.png
│ ├── logo-152.png
│ ├── logo-16.png
│ ├── logo-180.png
│ ├── logo-192.png
│ ├── logo-256.png
│ ├── logo-32.png
│ ├── logo-48.png
│ ├── logo-512.png
│ └── logo-64.png
├── robots.txt
└── sitemap.xml
├── src
├── App.vue
├── assets
│ └── style
│ │ ├── fonts.css
│ │ ├── style.css
│ │ └── transitions.css
├── components
│ ├── Buttons
│ │ └── PrimaryButton.vue
│ ├── FloatingMenu
│ │ ├── ActionMenu.vue
│ │ └── FloatingMenu.vue
│ ├── Icon
│ │ ├── MemoryLoadIcon.vue
│ │ ├── MemoryUnloadIcon.vue
│ │ ├── ModelIcon.vue
│ │ └── OllamaIcon.vue
│ ├── Lightbox
│ │ └── Lightbox.vue
│ ├── ModelSelect
│ │ ├── FilterMenu.vue
│ │ ├── ModelSelect.vue
│ │ ├── ModelSelectItem.vue
│ │ └── MultiItemSelect.vue
│ ├── Page
│ │ └── PageHeader.vue
│ ├── Popups
│ │ ├── NotConnectedPopup.vue
│ │ ├── Popup.vue
│ │ └── PopupLoader.vue
│ ├── Search
│ │ └── SearchBar.vue
│ ├── Sidebar
│ │ ├── ChatList.vue
│ │ ├── Sidebar.vue
│ │ ├── SidebarEntry.vue
│ │ ├── SidebarHeader.vue
│ │ ├── SidebarRouterLink.vue
│ │ └── footer
│ │ │ ├── SidebarFooter.vue
│ │ │ └── StatusText.vue
│ ├── TextDivider
│ │ └── TextDivider.vue
│ ├── Tooltip
│ │ └── Tooltip.vue
│ └── UtilSidebar
│ │ └── UtilSidebarItem.vue
├── composables
│ ├── useModelList.ts
│ └── usePWAState.ts
├── directives
│ ├── clickOutside.ts
│ └── mousedownOutside.ts
├── icons
│ ├── deepseek-color.svg
│ ├── deepseek.svg
│ ├── gemini-color.svg
│ ├── gemini.svg
│ ├── gemma-color.svg
│ ├── gemma.svg
│ ├── google-color.svg
│ ├── google.svg
│ ├── hunyuan-color.svg
│ ├── hunyuan.svg
│ ├── llamapen
│ │ └── favicon.svg
│ ├── llava-color.svg
│ ├── llava.svg
│ ├── meta-color.svg
│ ├── meta.svg
│ ├── microsoft-color.svg
│ ├── microsoft.svg
│ ├── mistral-color.svg
│ ├── mistral.svg
│ ├── moonshot-color.svg
│ ├── moonshot.svg
│ ├── nvidia-color.svg
│ ├── nvidia.svg
│ ├── openai-color.svg
│ ├── openai.svg
│ ├── qwen-color.svg
│ ├── qwen.svg
│ ├── together-color.svg
│ ├── together.svg
│ ├── unknown.svg
│ ├── zai-color.svg
│ └── zai.svg
├── layouts
│ ├── ChatLayout.vue
│ └── UtilityLayout.vue
├── lib
│ ├── db.ts
│ ├── logger.ts
│ ├── marked.ts
│ ├── mitt.ts
│ ├── router.ts
│ └── supabase.ts
├── main.ts
├── stores
│ ├── chatsStore.ts
│ ├── config.ts
│ ├── messagesStore.ts
│ ├── toolsStore.ts
│ ├── uiStore.ts
│ └── user.ts
├── types
│ ├── build.d.ts
│ ├── chat.d.ts
│ ├── ollama.d.ts
│ ├── toolsStore.d.ts
│ ├── types.d.ts
│ └── util.d.ts
├── utils
│ ├── core
│ │ ├── authedFetch.ts
│ │ ├── filesAsBase64.ts
│ │ ├── getDateTimeString.ts
│ │ ├── getMessageAttachments.ts
│ │ ├── isDateBeforeToday.ts
│ │ ├── isOnMobile.ts
│ │ ├── parseNumOrNull.ts
│ │ ├── promptDeleteChat.ts
│ │ ├── setPageTitle.ts
│ │ └── tryCatch.ts
│ ├── ollama.ts
│ ├── ollamaRequest.ts
│ └── streamChunks.ts
├── views
│ ├── account
│ │ ├── AccountPage.vue
│ │ └── components
│ │ │ ├── AccountSection.vue
│ │ │ └── ContactSection.vue
│ ├── chat
│ │ ├── ChatPage.vue
│ │ └── components
│ │ │ ├── ChatMessage
│ │ │ ├── ChatMessage.vue
│ │ │ ├── MessageInteractionButton.vue
│ │ │ ├── MessageInteractions.vue
│ │ │ ├── MessageModelSelector.vue
│ │ │ ├── MessageModelSelectorItem.vue
│ │ │ ├── ModelMessage
│ │ │ │ └── ModelMessageHeader.vue
│ │ │ ├── ModelToolCalls.vue
│ │ │ ├── ThinkBlock.vue
│ │ │ └── ToolCallsMessage.vue
│ │ │ ├── GreetingText.vue
│ │ │ ├── MessageEditor.vue
│ │ │ ├── MessageInput
│ │ │ ├── ActionButton.vue
│ │ │ ├── InputElem.vue
│ │ │ ├── MessageInput.vue
│ │ │ ├── ScrollToBottomButton.vue
│ │ │ └── buttons
│ │ │ │ ├── FileUpload.vue
│ │ │ │ ├── MessageInputButton.vue
│ │ │ │ ├── MessageOption.vue
│ │ │ │ ├── MessageOptions.vue
│ │ │ │ ├── MessageTools.vue
│ │ │ │ └── ThinkingButton.vue
│ │ │ └── MessageList.vue
│ ├── guide
│ │ └── GuidePage.vue
│ ├── models
│ │ ├── ModelsPage.vue
│ │ ├── components
│ │ │ ├── CapabilitiesSkeleton.vue
│ │ │ ├── DownloadManager.vue
│ │ │ ├── InfoSection.vue
│ │ │ ├── ModelList.vue
│ │ │ ├── ModelViewer.vue
│ │ │ ├── ModelsPageTypes.d.ts
│ │ │ └── ViewerContainer.vue
│ │ └── todo.md
│ ├── settings
│ │ ├── SettingsPage.vue
│ │ └── components
│ │ │ ├── CategoryLabel.vue
│ │ │ ├── NumberInputSetting.vue
│ │ │ ├── OptionCategory.vue
│ │ │ ├── OptionText.vue
│ │ │ ├── SelectionSetting.vue
│ │ │ ├── StatusIndicator.vue
│ │ │ ├── TextInputSetting.vue
│ │ │ └── ToggleSetting.vue
│ ├── shortcuts
│ │ ├── ShortcutsPage.vue
│ │ └── components
│ │ │ └── ShortcutDisplay.vue
│ └── tools
│ │ ├── SelectInput.vue
│ │ ├── TextInput.vue
│ │ ├── ToolEdit.vue
│ │ ├── ToolsList.vue
│ │ ├── ToolsPage.vue
│ │ └── edit
│ │ └── ToolRequestOptions.vue
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json
└── vite.config.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .git
4 | .vscode
5 | dev-dist
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | VITE_DEFAULT_OLLAMA=http://127.0.0.1:11434
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [ImDarkTom]
2 | buy_me_a_coffee: ImDarkTom
3 | # polar: # Replace with a single Polar username e.g., user1
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .env
27 | # Dev
28 | icons_all/
29 |
30 | dev-dist/
31 | .vercel
32 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "bradlc.vscode-tailwindcss"]
3 | }
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build Stage
2 | FROM oven/bun:1.2 AS build
3 |
4 | WORKDIR /app
5 |
6 | COPY bun.lock package.json ./
7 |
8 | RUN bun install --frozen-lockfile
9 |
10 | COPY . .
11 |
12 | RUN bunx vite build
13 | # TODO: make it type-check before building using `bun run build`
14 |
15 | # Production Stage
16 | FROM nginx:stable-alpine AS production
17 |
18 | COPY --from=build /app/dist /usr/share/nginx/html
19 |
20 | COPY nginx.conf /etc/nginx/conf.d/default.conf
21 |
22 | EXPOSE 80
23 |
24 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LlamaPen
2 |
3 | A no-install needed GUI for Ollama.
4 |
5 | 
6 |
7 | ## Features
8 |
9 | - 🌐 Web-based interface accessible on both desktop and mobile.
10 | - ✅ Easy setup & configuration.
11 | - 🛠️ Renders markdown, think text, LaTeX math.
12 | - ⚡ Keyboard shortcuts for quick navigation.
13 | - 🗃️ Built-in model & download manager.
14 | - 🔌 Offline & PWA support.
15 | - 🕊️ 100% Free & Open-Source.
16 |
17 | ## Setting Up
18 |
19 | A [guide for setup](https://llamapen.app/guide) is included on the site. We've tried to make setup as smooth and straightforward as possible, letting you configure once and immediately start chatting any time Ollama is running.
20 |
21 | Once set-up, you can start chatting. All **chats are stored locally** in your browser giving you complete privacy and near-instant chat load times.
22 |
23 | ## Contributing/Running Locally
24 |
25 | Contributing/running locally is also made as straightforward as possible. To get a local version of LlamaPen running on your machine, follow these steps:
26 |
27 | ### 0. Prerequisites
28 |
29 | Make sure you have installed:
30 |
31 | - [Git](https://git-scm.com/downloads)
32 | - [Bun](https://bun.sh/) (1.2+)
33 |
34 | ### 1. Download
35 |
36 | ```bash
37 | git clone https://github.com/ImDarkTom/LlamaPen
38 | cd LlamaPen
39 | ```
40 |
41 | ### 2. Install dependencies
42 |
43 | ```bash
44 | bun i
45 | ```
46 |
47 | ### 3. Run
48 |
49 | If you want to run in **developer mode** and see changes in your code updated live, do:
50 |
51 | ```bash
52 | bun dev
53 | ```
54 |
55 | If you want to just **run locally** with no overhead, do:
56 |
57 | ```bash
58 | bun run local
59 | ```
60 |
61 | *That's it!* If you are contributing and using VSCode you can optionally install the extensions in the `extensions.json` file for a smoother development experience.
62 |
63 | ## LlamaPen API
64 |
65 | If you are using the [official site](https://llamapen.app/) (`https://llamapen.app`), you can **optionally** enable LlamaPen API. LlamaPen API is a cloud service that lets you run the most powerful version of up-to-date models if you are not able to run them locally. Note that while LlamaPen is free and open-source, LlamaPen API offers an optional subscription for increasing rate limits and accessing more expensive models.
66 |
67 | For security purposes, LlamaPen API is **not** open-source, however we strive to ensure your privacy (as outlined in the API [privacy policy](https://api.llamapen.app/privacy)), and the only time we have access to your chats is when you explicitly enable LlamaPen API in the settings and send a chat request using one of the models. If you do not want to use this, keeping the toggle off will ensure that **no data is ever sent** to LlamaPen API servers.
68 |
69 | ## Donating
70 |
71 | Funding to help development is always appreciated, whether that is through purchasing a subscription on LlamaPen API or donating directly, I will appreciate any sponsorship you give.
72 |
73 |
74 |
75 | ## Licenses & Attribution
76 |
77 | - [Ollama](https://github.com/ollama/ollama)
78 | - [Lobe Icons](https://github.com/lobehub/lobe-icons)
79 | - [Nebula Sans Font](https://www.nebulasans.com/)
80 | - [*Picture in the preview*](https://commons.wikimedia.org/w/index.php?curid=145806133)
81 |
82 | *LlamaPen* is [AGPL-3.0](https://github.com/ImDarkTom/LlamaPen?tab=AGPL-3.0-1-ov-file)
83 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // eslint.config.mjs
2 | import pluginVue from 'eslint-plugin-vue'
3 | import {
4 | defineConfigWithVueTs,
5 | vueTsConfigs,
6 | configureVueProject,
7 | } from '@vue/eslint-config-typescript'
8 |
9 | configureVueProject({
10 | tsSyntaxInTemplates: true,
11 | scriptLangs: [
12 | 'ts',
13 | ],
14 | allowComponentTypeUnsafety: true,
15 | rootDir: import.meta.dirname,
16 | })
17 |
18 | export default defineConfigWithVueTs(
19 | pluginVue.configs["flat/essential"],
20 | vueTsConfigs.recommended,
21 | {
22 | rules: {
23 | "@typescript-eslint/no-unused-vars": [
24 | "error",
25 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }
26 | ],
27 | }
28 | }
29 | )
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ config.ollamaUrl }}
. Ensure Ollama is running and
63 | accepts connection requests from this site.
64 |
37 |
${codeHtml}\n
`;
59 | }
60 | } as RendererObject;
61 |
62 | const fullMarked = new Marked();
63 |
64 | fullMarked.use({ renderer });
65 |
66 | fullMarked.use(markedKatex());
67 |
68 | /**
69 | * Handles rendering markdown, using DOMPurify to prevent XSS. Note: This may be ran many times
70 | * concurrently when text is being generated so keep performance nominal.
71 | * @param text The markdown to be rendered as HTML
72 | * @returns Markdown as sanitized rendered HTML
73 | */
74 | export function renderMarkdown(text: string) {
75 | const rawHtml = fullMarked.parse(text, { async: false });
76 | const sanitizedHtml = DOMPurify.sanitize(rawHtml, { ADD_ATTR: ['target'] });
77 |
78 | return sanitizedHtml;
79 | }
80 |
--------------------------------------------------------------------------------
/src/lib/mitt.ts:
--------------------------------------------------------------------------------
1 | import mitt from "mitt";
2 |
3 | export enum PopupButtons {
4 | CLOSE,
5 | OK_CANCEL
6 | }
7 |
8 | type Events = {
9 | scrollToBottom: {
10 | force: boolean,
11 | },
12 | openNotConnectedPopup: void,
13 | openLightbox: {
14 | image: File | Blob,
15 | },
16 | openSearchbox: void,
17 | openChat: string,
18 | stopChatGeneration: void,
19 | hideSidebar: void,
20 | focusInputBar: void,
21 | };
22 |
23 | export const emitter = mittOllama does not allow connections from any external URLs by default, so for this app to work you need to add this app's URL to 41 | Ollama's trusted origins and re-launch it.
42 |Below are guides on how to do that on set different operating systems:
44 | 45 |On Windows you can do this by running this command in Command Prompt or PowerShell
49 |set OLLAMA_ORIGINS="{{ originUrl }}" & ollama serve
53 | This will temporarily allow connections to Ollama from this URL until Ollama is closed.
56 |If you want to be able to connect without re-running this command each time, you can instead run 59 | another command to persistently add this app's URL to Ollama's trusted origins:
60 |setx OLLAMA_ORIGINS "{{ originUrl }}"
63 | After that, just open Ollama normally and you should be able to connect after refreshing this page.
66 | 67 |On linux or MacOS, you can run a similar command:
72 |export OLLAMA_ORIGINS="{{ originUrl }}" && ollama serve
75 | And similarly to persistently add to trusted origins you can do:
78 |echo 'export OLLAMA_ORIGINS="{{ originUrl }}"' >> ~/.bashrc && source ~/.bashrc
81 | (This is assuming you are using Bash, other shells may have different ways of setting global variables)
84 | 85 |The most common cause for this error is that Ollama is already running. Make sure to close Ollama/end the
93 | process before trying again.
94 |
95 | This may also happen if another program is running on the same port which is unlikely, however it is possible.
96 |