├── .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 | ![Preview](https://github.com/user-attachments/assets/5243a538-db02-4296-9baf-70f99a566b8c) 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 | Buy Me A Coffee 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 | 5 | 6 | 7 | LlamaPen 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | 5 | root /usr/share/nginx/html; 6 | index index.html; 7 | 8 | location / { 9 | try_files $uri /index.html; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llamapen", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "bunx --bun vite", 8 | "build": "vue-tsc -b && vite build", 9 | "preview": "vite preview", 10 | "local": "bun run build && echo \"✅ App should be running locally at http://localhost:8080\" && bunx --bun serve dist --listen 8080 --single" 11 | }, 12 | "dependencies": { 13 | "@supabase/supabase-js": "^2.55.0", 14 | "dexie": "^4.0.11", 15 | "dompurify": "^3.2.6", 16 | "highlight.js": "^11.11.1", 17 | "katex": "^0.16.22", 18 | "marked": "^15.0.12", 19 | "marked-katex-extension": "^5.1.5", 20 | "mitt": "^3.0.1", 21 | "normalize.css": "^8.0.1", 22 | "path": "^0.12.7", 23 | "pinia": "^2.3.1", 24 | "pinia-plugin-persistedstate": "^4.5.0", 25 | "readable-stream": "^4.7.0", 26 | "uuid": "^11.1.0", 27 | "vue": "^3.5.18", 28 | "vue-icons-plus": "^0.1.8", 29 | "vue-router": "^4.5.1" 30 | }, 31 | "devDependencies": { 32 | "@tailwindcss/typography": "^0.5.16", 33 | "@tailwindcss/vite": "^4.1.12", 34 | "@types/node": "^22.17.1", 35 | "@types/readable-stream": "^4.0.21", 36 | "@vitejs/plugin-vue": "^5.2.4", 37 | "@vue/eslint-config-typescript": "^14.6.0", 38 | "@vue/tsconfig": "^0.7.0", 39 | "tailwindcss": "^4.1.12", 40 | "tailwindcss-motion": "^1.1.1", 41 | "typescript": "~5.6.3", 42 | "vite": "^6.3.5", 43 | "vite-plugin-pwa": "^1.0.2", 44 | "vite-svg-loader": "^5.1.0", 45 | "vue-tsc": "^2.2.12" 46 | } 47 | } -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-Black.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-BlackItalic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-Bold.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-BoldItalic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-Book.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-BookItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-BookItalic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-Light.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-LightItalic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-Medium.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-MediumItalic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-Semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-Semibold.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/NebulaSans-SemiboldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/assets/fonts/NebulaSans-SemiboldItalic.woff2 -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/logo-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-120.png -------------------------------------------------------------------------------- /public/icons/logo-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-152.png -------------------------------------------------------------------------------- /public/icons/logo-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-16.png -------------------------------------------------------------------------------- /public/icons/logo-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-180.png -------------------------------------------------------------------------------- /public/icons/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-192.png -------------------------------------------------------------------------------- /public/icons/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-256.png -------------------------------------------------------------------------------- /public/icons/logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-32.png -------------------------------------------------------------------------------- /public/icons/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-48.png -------------------------------------------------------------------------------- /public/icons/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-512.png -------------------------------------------------------------------------------- /public/icons/logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/public/icons/logo-64.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Disallow: /chat/* 5 | Disallow: /settings 6 | Disallow: /account 7 | 8 | Sitemap: https://llamapen.app/sitemap.xml 9 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://llamapen.app/ 5 | weekly 6 | 1.0 7 | 8 | 9 | 10 | https://llamapen.app/guide 11 | weekly 12 | 0.8 13 | 14 | 15 | 16 | https://llamapen.app/shortcuts 17 | monthly 18 | 0.7 19 | 20 | 21 | 22 | https://llamapen.app/models 23 | weekly 24 | 0.5 25 | 26 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/style/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Nebula Sans'; 3 | src: url('/assets/fonts/NebulaSans-Light.woff2') format('woff2'); 4 | font-weight: 300; 5 | font-style: normal; 6 | font-display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: 'Nebula Sans'; 11 | src: url('/assets/fonts/NebulaSans-LightItalic.woff2') format('woff2'); 12 | font-weight: 300; 13 | font-style: italic; 14 | font-display: swap; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Nebula Sans'; 19 | src: url('/assets/fonts/NebulaSans-Book.woff2') format('woff2'); 20 | font-weight: 400; 21 | font-style: normal; 22 | font-display: swap; 23 | } 24 | 25 | @font-face { 26 | font-family: 'Nebula Sans'; 27 | src: url('/assets/fonts/NebulaSans-BookItalic.woff2') format('woff2'); 28 | font-weight: 400; 29 | font-style: italic; 30 | font-display: swap; 31 | } 32 | 33 | @font-face { 34 | font-family: 'Nebula Sans'; 35 | src: url('/assets/fonts/NebulaSans-Medium.woff2') format('woff2'); 36 | font-weight: 500; 37 | font-style: normal; 38 | font-display: swap; 39 | } 40 | 41 | @font-face { 42 | font-family: 'Nebula Sans'; 43 | src: url('/assets/fonts/NebulaSans-MediumItalic.woff2') format('woff2'); 44 | font-weight: 500; 45 | font-style: italic; 46 | font-display: swap; 47 | } 48 | 49 | @font-face { 50 | font-family: 'Nebula Sans'; 51 | src: url('/assets/fonts/NebulaSans-Semibold.woff2') format('woff2'); 52 | font-weight: 600; 53 | font-style: normal; 54 | font-display: swap; 55 | } 56 | 57 | @font-face { 58 | font-family: 'Nebula Sans'; 59 | src: url('/assets/fonts/NebulaSans-SemiboldItalic.woff2') format('woff2'); 60 | font-weight: 600; 61 | font-style: italic; 62 | font-display: swap; 63 | } 64 | 65 | @font-face { 66 | font-family: 'Nebula Sans'; 67 | src: url('/assets/fonts/NebulaSans-Bold.woff2') format('woff2'); 68 | font-weight: 700; 69 | font-style: normal; 70 | font-display: swap; 71 | } 72 | 73 | @font-face { 74 | font-family: 'Nebula Sans'; 75 | src: url('/assets/fonts/NebulaSans-BoldItalic.woff2') format('woff2'); 76 | font-weight: 700; 77 | font-style: italic; 78 | font-display: swap; 79 | } 80 | 81 | @font-face { 82 | font-family: 'Nebula Sans'; 83 | src: url('/assets/fonts/NebulaSans-Black.woff2') format('woff2'); 84 | font-weight: 900; 85 | font-style: normal; 86 | font-display: swap; 87 | } 88 | 89 | @font-face { 90 | font-family: 'Nebula Sans'; 91 | src: url('/assets/fonts/NebulaSans-BlackItalic.woff2') format('woff2'); 92 | font-weight: 900; 93 | font-style: italic; 94 | font-display: swap; 95 | } 96 | -------------------------------------------------------------------------------- /src/assets/style/transitions.css: -------------------------------------------------------------------------------- 1 | /* for testing: @media all { */ 2 | @media (prefers-reduced-motion: reduce) { 3 | * { 4 | transition: none !important; 5 | transition-duration: 0s; 6 | } 7 | } 8 | 9 | body[data-reduce-motion] { 10 | * { 11 | transition: none !important; 12 | transition-duration: 0s; 13 | } 14 | } 15 | 16 | /* page switch - for utils layout */ 17 | .page-switch-enter-active, 18 | .page-switch-leave-active { 19 | transition: opacity var(--transition-duration) ease, transform var(--transition-duration) ease, filter var(--transition-duration) ease; 20 | z-index: -100; 21 | } 22 | 23 | .page-switch-enter-from, 24 | .page-switch-leave-to { 25 | transform: translateY(10%) scale(0.95); 26 | opacity: 0; 27 | filter: blur(0.1rem); 28 | } 29 | 30 | /* expand height - for think block */ 31 | .expand-height-enter-active, 32 | .expand-height-leave-active { 33 | transition: transform var(--transition-duration) ease; 34 | transform-origin: top; 35 | } 36 | 37 | .expand-height-enter-from, 38 | .expand-height-leave-to { 39 | transform: scaleY(0) translateY(-10%); 40 | transform-origin: top; 41 | } 42 | 43 | /* layout-to-chat & layout-to-utility - for layout switches */ 44 | .layout-to-chat-enter-active, 45 | .layout-to-chat-leave-active, 46 | .layout-to-utility-enter-active, 47 | .layout-to-utility-leave-active { 48 | transition: opacity var(--transition-duration) ease, transform var(--transition-duration) ease; 49 | z-index: -100; 50 | } 51 | .layout-to-chat-leave-to, 52 | .layout-to-utility-enter-from { 53 | transform: scale(0.75) translateY(100%); 54 | opacity: 0; 55 | } 56 | 57 | .layout-to-chat-enter-from, 58 | .layout-to-utility-leave-to { 59 | transform: scale(1) translateY(0%); 60 | opacity: 0; 61 | } -------------------------------------------------------------------------------- /src/components/Buttons/PrimaryButton.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/FloatingMenu/ActionMenu.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/Icon/MemoryLoadIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/Icon/MemoryUnloadIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/Icon/ModelIcon.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/Icon/OllamaIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Lightbox/Lightbox.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/ModelSelect/FilterMenu.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | -------------------------------------------------------------------------------- /src/components/Page/PageHeader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Popups/NotConnectedPopup.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/components/Popups/Popup.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | -------------------------------------------------------------------------------- /src/components/Popups/PopupLoader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/Sidebar/ChatList.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarHeader.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarRouterLink.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/Sidebar/footer/SidebarFooter.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Sidebar/footer/StatusText.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/TextDivider/TextDivider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/UtilSidebar/UtilSidebarItem.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/composables/usePWAState.ts: -------------------------------------------------------------------------------- 1 | import { useRegisterSW } from 'virtual:pwa-register/vue'; 2 | import { ref } from 'vue'; 3 | 4 | export function usePWAState() { 5 | const { offlineReady, needRefresh, updateServiceWorker } = useRegisterSW({ 6 | onOfflineReady() { 7 | }, 8 | onNeedRefresh() { 9 | }, 10 | }); 11 | 12 | const isOnline = ref(navigator.onLine); 13 | 14 | window.addEventListener('online', () => (isOnline.value = true)); 15 | window.addEventListener('offline', () => (isOnline.value = false)); 16 | 17 | return { 18 | isOnline, 19 | offlineReady, 20 | needRefresh, 21 | updateServiceWorker, 22 | }; 23 | } -------------------------------------------------------------------------------- /src/directives/clickOutside.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from "vue"; 2 | 3 | // based off https://stackoverflow.com/a/76281017 4 | const clickOutside: Directive = { 5 | beforeMount: (element, binding) => { 6 | element.clickOutsideEvent = function (event: MouseEvent) { 7 | if (!(element === event.target || element.contains(event.target))) { 8 | binding.value(event); 9 | } 10 | }; 11 | 12 | document.body.addEventListener('click', element.clickOutsideEvent); 13 | }, 14 | unmounted: (element) => { 15 | document.body.removeEventListener('click', element.clickOutsideEvent); 16 | } 17 | }; 18 | 19 | export default clickOutside; -------------------------------------------------------------------------------- /src/directives/mousedownOutside.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from "vue"; 2 | 3 | // https://stackoverflow.com/a/76281017 4 | const mousedownOutside: Directive = { 5 | beforeMount: (element, binding) => { 6 | element.mousedownOutsideEvent = function (event: MouseEvent) { 7 | if (!(element === event.target || element.contains(event.target))) { 8 | binding.value(event); 9 | } 10 | }; 11 | 12 | document.body.addEventListener('mousedown', element.mousedownOutsideEvent); 13 | }, 14 | unmounted: (element) => { 15 | document.body.removeEventListener('mousedown', element.mousedownOutsideEvent); 16 | } 17 | }; 18 | 19 | export default mousedownOutside; -------------------------------------------------------------------------------- /src/icons/deepseek-color.svg: -------------------------------------------------------------------------------- 1 | DeepSeek -------------------------------------------------------------------------------- /src/icons/deepseek.svg: -------------------------------------------------------------------------------- 1 | DeepSeek -------------------------------------------------------------------------------- /src/icons/gemini-color.svg: -------------------------------------------------------------------------------- 1 | Gemini -------------------------------------------------------------------------------- /src/icons/gemini.svg: -------------------------------------------------------------------------------- 1 | Gemini -------------------------------------------------------------------------------- /src/icons/gemma-color.svg: -------------------------------------------------------------------------------- 1 | Gemma -------------------------------------------------------------------------------- /src/icons/gemma.svg: -------------------------------------------------------------------------------- 1 | Gemma -------------------------------------------------------------------------------- /src/icons/google-color.svg: -------------------------------------------------------------------------------- 1 | Google -------------------------------------------------------------------------------- /src/icons/google.svg: -------------------------------------------------------------------------------- 1 | Google -------------------------------------------------------------------------------- /src/icons/hunyuan-color.svg: -------------------------------------------------------------------------------- 1 | Hunyuan -------------------------------------------------------------------------------- /src/icons/hunyuan.svg: -------------------------------------------------------------------------------- 1 | Hunyuan -------------------------------------------------------------------------------- /src/icons/llamapen/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/meta.svg: -------------------------------------------------------------------------------- 1 | Meta -------------------------------------------------------------------------------- /src/icons/microsoft-color.svg: -------------------------------------------------------------------------------- 1 | Azure -------------------------------------------------------------------------------- /src/icons/microsoft.svg: -------------------------------------------------------------------------------- 1 | Azure -------------------------------------------------------------------------------- /src/icons/mistral-color.svg: -------------------------------------------------------------------------------- 1 | Mistral -------------------------------------------------------------------------------- /src/icons/mistral.svg: -------------------------------------------------------------------------------- 1 | Mistral -------------------------------------------------------------------------------- /src/icons/moonshot-color.svg: -------------------------------------------------------------------------------- 1 | MoonshotAI -------------------------------------------------------------------------------- /src/icons/moonshot.svg: -------------------------------------------------------------------------------- 1 | MoonshotAI -------------------------------------------------------------------------------- /src/icons/nvidia-color.svg: -------------------------------------------------------------------------------- 1 | Nvidia -------------------------------------------------------------------------------- /src/icons/nvidia.svg: -------------------------------------------------------------------------------- 1 | Nvidia -------------------------------------------------------------------------------- /src/icons/openai-color.svg: -------------------------------------------------------------------------------- 1 | OpenAI -------------------------------------------------------------------------------- /src/icons/openai.svg: -------------------------------------------------------------------------------- 1 | OpenAI -------------------------------------------------------------------------------- /src/icons/qwen-color.svg: -------------------------------------------------------------------------------- 1 | Qwen -------------------------------------------------------------------------------- /src/icons/qwen.svg: -------------------------------------------------------------------------------- 1 | Qwen -------------------------------------------------------------------------------- /src/icons/together-color.svg: -------------------------------------------------------------------------------- 1 | together.ai -------------------------------------------------------------------------------- /src/icons/together.svg: -------------------------------------------------------------------------------- 1 | together.ai -------------------------------------------------------------------------------- /src/icons/unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 10 | 11 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /src/icons/zai-color.svg: -------------------------------------------------------------------------------- 1 | Z.ai -------------------------------------------------------------------------------- /src/icons/zai.svg: -------------------------------------------------------------------------------- 1 | Z.ai -------------------------------------------------------------------------------- /src/layouts/ChatLayout.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/layouts/UtilityLayout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import Dexie, { type EntityTable } from 'dexie'; 2 | 3 | const db = new Dexie('LlamapenDB') as Dexie & { 4 | chats: EntityTable, 5 | messages: EntityTable, 6 | attachments: EntityTable, 7 | }; 8 | 9 | db.version(1).stores({ 10 | chats: '++id,title,createdAt,lastestMessageDate,pinned', 11 | messages: '++id,chatId,created,[chatId+id]', 12 | attachments: '++id,messageId,created' 13 | }); 14 | 15 | export default db; -------------------------------------------------------------------------------- /src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | class Logger { 2 | private shouldLog: boolean; 3 | 4 | constructor() { 5 | this.shouldLog = import.meta.env.MODE === 'development'; 6 | } 7 | 8 | info(origin: string, ...data: any) { 9 | if (!this.shouldLog) return; 10 | 11 | const currentTime = new Date().toLocaleString(); 12 | 13 | console.info( 14 | `[${currentTime}] [%c${origin}%c] [%cINFO%c]`, 15 | 'color: lightblue', '', 'color: #BBBBBB', '', 16 | ...data 17 | ); 18 | } 19 | 20 | warn(origin: string, ...data: any) { 21 | const currentTime = new Date().toLocaleString(); 22 | 23 | console.warn( 24 | `[${currentTime}] [%c${origin}%c] [%cWARN%c]`, 25 | 'color: lightblue', '', 'color: orange', '', 26 | ...data 27 | ); 28 | } 29 | } 30 | 31 | const logger = new Logger(); 32 | 33 | export default logger; -------------------------------------------------------------------------------- /src/lib/marked.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js'; 2 | import { Marked, type RendererObject } from 'marked'; 3 | import markedKatex from 'marked-katex-extension'; 4 | import DOMPurify from 'dompurify'; 5 | 6 | import "katex/dist/katex.min.css"; 7 | import "highlight.js/styles/github-dark.min.css"; 8 | 9 | function escape(html: string, encode = false) { 10 | const escapeReplacements: Record = { 11 | '&': '&', 12 | '<': '<', 13 | '>': '>', 14 | '"': '"', 15 | "'": ''', 16 | }; 17 | if (encode) return html.replace(/[&<>"']/g, (ch) => escapeReplacements[ch]); 18 | return html; 19 | } 20 | 21 | const renderer = { 22 | link(token: any) { 23 | const href = token.href; 24 | const title = token.title; 25 | const text = token.text || href; 26 | 27 | const isInternal = 28 | href.startsWith('/') || 29 | href.startsWith('#') || 30 | href.startsWith(window.location.origin); 31 | 32 | const titleAttr = title ? `title="${title}"` : ''; 33 | const targetAttrs = isInternal ? '' : 'target="_blank" rel="noopener noreferrer"'; 34 | const externalIndicator = isInternal ? '' : ' ↗'; 35 | 36 | return `${text}${externalIndicator}`; 37 | }, 38 | code(token: { lang: string, text: string, raw: string, type: string }) { 39 | const lang = token.lang || ''; 40 | const language = hljs.getLanguage(lang) ? lang : ''; 41 | const languagePretty = hljs.getLanguage(lang)?.name || language; 42 | 43 | const highlighted = language 44 | ? hljs.highlight(token.text, { language }).value 45 | : escape(token.text, true); 46 | 47 | const classValue = language ? `hljs language-${language}` : 'hljs'; // add language to class if valid 48 | 49 | const codeHtml = highlighted.replace(/\n$/, ''); 50 | 51 | return ` 52 |
53 | ${languagePretty} 54 | 57 |
58 |
${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 = mitt(); -------------------------------------------------------------------------------- /src/lib/router.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; 3 | import ChatPage from "@/views/chat/ChatPage.vue"; 4 | import SettingsPage from "@/views/settings/SettingsPage.vue"; 5 | import GuidePage from "@/views/guide/GuidePage.vue"; 6 | import AccountPage from '@/views/account/AccountPage.vue'; 7 | import ShortcutsPage from '@/views/shortcuts/ShortcutsPage.vue'; 8 | import ModelsPage from '@/views/models/ModelsPage.vue'; 9 | import ChatLayout from '@/layouts/ChatLayout.vue'; 10 | import ToolsPage from '@/views/tools/ToolsPage.vue'; 11 | import UtilityLayout from '@/layouts/UtilityLayout.vue'; 12 | 13 | const routes: RouteRecordRaw[] = [ 14 | { 15 | path: "/", 16 | component: ChatLayout, 17 | meta: { layer: "chat" }, 18 | children: [ 19 | { path: "/", component: ChatPage }, 20 | { 21 | path: "/chat", 22 | component: ChatPage, 23 | children: [ 24 | { path: "/chat/:id", component: ChatPage } 25 | ] 26 | }, 27 | ], 28 | }, 29 | { 30 | path: '/', 31 | component: UtilityLayout, 32 | meta: { layer: 'utility' }, 33 | children: [ 34 | { path: '/settings', component: SettingsPage }, 35 | { path: "/shortcuts", component: ShortcutsPage }, 36 | { path: '/guide', component: GuidePage }, 37 | { path: '/account', component: AccountPage }, 38 | { 39 | path: '/models', 40 | component: ModelsPage, 41 | children: [ 42 | { path: '/models/:model(.*)', component: ModelsPage } 43 | ] 44 | }, 45 | { 46 | path: '/tools', 47 | component: ToolsPage, 48 | children: [ 49 | { path: '/tools/:tool(.*)', component: ToolsPage } 50 | ] 51 | }, 52 | ] 53 | } 54 | ]; 55 | 56 | const router = createRouter({ 57 | history: createWebHistory(), 58 | routes, 59 | }); 60 | 61 | export default router; -------------------------------------------------------------------------------- /src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | class SupabaseClient { 4 | private client: ReturnType | null = null; 5 | 6 | initialise() { 7 | if (import.meta.env.VITE_PRODUCTION !== 'true') { 8 | return null; 9 | } 10 | 11 | const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; 12 | const supabaseKey = import.meta.env.VITE_SUPABASE_PUBLIC_KEY; 13 | 14 | this.client = createClient(supabaseUrl, supabaseKey); 15 | } 16 | 17 | get supabase() { 18 | return this.client; 19 | } 20 | } 21 | 22 | const supabase = new SupabaseClient(); 23 | supabase.initialise(); 24 | 25 | export default supabase.supabase; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./assets/style/style.css"; 3 | import "./assets/style/fonts.css"; 4 | import "./assets/style/transitions.css"; 5 | import App from "./App.vue"; 6 | import { createPinia } from "pinia"; 7 | import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; 8 | 9 | import router from './lib/router'; 10 | import clickOutside from "./directives/clickOutside"; 11 | 12 | const pinia = createPinia(); 13 | 14 | pinia.use(piniaPluginPersistedstate); 15 | 16 | const app = createApp(App); 17 | app.use(router); 18 | app.use(pinia); 19 | 20 | app.directive('click-outside', clickOutside); 21 | 22 | app.mount("#app"); 23 | -------------------------------------------------------------------------------- /src/stores/uiStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | interface UIStore { 4 | chat: { 5 | isScrollingDown: boolean, 6 | } 7 | } 8 | 9 | /** 10 | * Misc util state variables used throughout UI. 11 | */ 12 | export const useUiStore = defineStore('uiStore', { 13 | state: (): UIStore => ({ 14 | chat: { 15 | isScrollingDown: true, 16 | } 17 | }), 18 | getters: {}, 19 | actions: {} 20 | }); -------------------------------------------------------------------------------- /src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import supabase from '@/lib/supabase'; 2 | import { authedFetch } from '@/utils/core/authedFetch'; 3 | import type { Session, User } from '@supabase/supabase-js'; 4 | import { defineStore } from 'pinia'; 5 | import { computed, ref } from 'vue'; 6 | import { useConfigStore } from './config'; 7 | import logger from '@/lib/logger'; 8 | 9 | const inProduction = import.meta.env.VITE_PRODUCTION === 'true'; 10 | 11 | async function fetchUser() { 12 | const userResponse = await supabase?.auth.getUser(); 13 | user.value = userResponse?.data.user || null; 14 | 15 | logger.info('User Store', 'User data set to', user.value); 16 | } 17 | 18 | async function fetchSession() { 19 | const userResponse = await supabase?.auth.getSession(); 20 | session.value = userResponse?.data.session || null; 21 | } 22 | 23 | async function fetchSubInfo() { 24 | const subscriptionResponse = await authedFetch(useConfigStore().apiUrl('/user/subscription-info')); 25 | if (!subscriptionResponse) return; 26 | 27 | const subscriptionInfoResponse = await subscriptionResponse.json(); 28 | subscriptionInfo.value = subscriptionInfoResponse; 29 | } 30 | 31 | const user = ref(null); 32 | const session = ref(null); 33 | const subscriptionInfo = ref<{ 34 | name: string, 35 | subscribed: boolean, 36 | status?: string, 37 | period_end: number, 38 | cancel_at_period_end: boolean, 39 | usage: { 40 | limit: number, 41 | remaining: number, 42 | lastUpdated: string | null; 43 | } 44 | }>({ 45 | name: 'Loading...', 46 | subscribed: false, 47 | usage: { 48 | limit: 20, 49 | remaining: 20, 50 | lastUpdated: null, 51 | }, 52 | period_end: -1, 53 | cancel_at_period_end: false 54 | }); 55 | 56 | /** 57 | * Store to manage auth with LlamaPen account. 58 | */ 59 | const useUserStore = defineStore('user', () => { 60 | // If in prod, api is enabled, and user info not already loaded. 61 | if (inProduction && useConfigStore().api.enabled && user.value === null) { 62 | fetchUser(); 63 | fetchSession(); 64 | fetchSubInfo(); 65 | } 66 | 67 | const isSignedIn = computed(() => user.value !== null); 68 | const subscription = computed(() => subscriptionInfo.value); 69 | 70 | const refreshSubInfo = fetchSubInfo; 71 | 72 | return { user, subscription, isSignedIn, refreshSubInfo }; 73 | }); 74 | 75 | export default useUserStore; -------------------------------------------------------------------------------- /src/types/build.d.ts: -------------------------------------------------------------------------------- 1 | declare const __COMMIT_HASH__: string 2 | declare const __APP_VERSION__: string -------------------------------------------------------------------------------- /src/types/chat.d.ts: -------------------------------------------------------------------------------- 1 | interface Chat { 2 | id: number; 3 | title: string; 4 | createdAt: Date; 5 | lastestMessageDate?: Date; 6 | isGenerating?: boolean; 7 | pinned: 0 | 1; 8 | } 9 | 10 | type UserAttachment = { 11 | id: number; 12 | messageId: number; 13 | created: Date; 14 | content: Blob; 15 | } 16 | 17 | type ChatMessage = ModelChatMessage | UserChatMessage | ToolChatMessage; 18 | 19 | type BaseChatMessage = { 20 | id: number; 21 | chatId: number; 22 | content: string; 23 | created: Date; 24 | }; 25 | 26 | interface ModelChatMessage extends BaseChatMessage { 27 | type: 'model'; 28 | model: string; 29 | thinking?: string; 30 | status: ModelMessageStatus; 31 | toolCalls?: { 32 | function: { 33 | name: string; 34 | arguments: Record, 35 | } 36 | }[]; 37 | stats?: { 38 | evalCount?: number; 39 | evalDuration?: number; 40 | loadDuration?: number; 41 | promptEvalCount?: number; 42 | promptEvalDuration?: number; 43 | totalDuration?: number; 44 | }; 45 | thinkStats?: { 46 | started?: number; 47 | ended?: number; 48 | }; 49 | } 50 | 51 | type ModelMessageStatus = 'waiting' | 'generating' | 'finished' | 'cancelled'; 52 | 53 | interface UserChatMessage extends BaseChatMessage { 54 | type: 'user'; 55 | } 56 | 57 | interface ToolChatMessage extends BaseChatMessage { 58 | type: 'tool', 59 | toolName: string; 60 | completed?: Date; 61 | } -------------------------------------------------------------------------------- /src/types/ollama.d.ts: -------------------------------------------------------------------------------- 1 | type OllamaToolParamSchema = Record; 10 | 11 | type ModelListItem = { 12 | name: string; 13 | model: string; 14 | modified_at: string; 15 | size: number; 16 | digest: string; 17 | details: { 18 | format: string; 19 | family: string; 20 | families: string[] | null; 21 | parameter_size: string; 22 | quantization_level: string; 23 | }; 24 | capabilities?: ('completion' | 'tools' | 'thinking' | 'vision' | 'insert' | 'embedding' | 'search')[]; 25 | llamapenMetadata?: { 26 | premium?: boolean; 27 | creator: string; 28 | tags?: string[]; 29 | } 30 | } 31 | 32 | type OllamaCapability = NonNullable[number]; 33 | 34 | type ModelList = ModelListItem[]; 35 | 36 | type ModelListResponse = { 37 | models: ModelList; 38 | } 39 | 40 | type CustomErrorResponse = { 41 | error: string; 42 | } 43 | 44 | type OllamaChatResponseChunk = { 45 | model: string; 46 | created_at: string; 47 | message: { 48 | role: MessageRole; 49 | content: string; 50 | thinking?: string; 51 | tool_calls?: OllamaToolCall[]; 52 | }; 53 | done: boolean; 54 | done_reason?: 'stop' | string; 55 | total_duration?: number; 56 | load_duration?: number; 57 | prompt_eval_count?: number; 58 | prompt_eval_duration?: number; 59 | eval_count?: number; 60 | eval_duration?: number; 61 | } 62 | 63 | type OllamaModelInfoResponse = { 64 | license: string; 65 | modelfile: string; 66 | template: string; 67 | details: { 68 | parent_model: string; 69 | format: string; 70 | family: string; 71 | families: string[]; 72 | parameter_size: string; 73 | quantization_level: string; 74 | }; 75 | model_info: { 76 | 'general.architecture': string; 77 | 'general.basename': string; 78 | 'general.file_type': number; 79 | 'general.finetune': string; 80 | 'general.languages': unknown | null; 81 | 'general.parameter_count': number; 82 | 'general.quantization_version': number; 83 | 'general.size_label': string; 84 | 'general.tags': unknown | null; 85 | 'general.type': string; 86 | } & Record; 87 | tensors: { 88 | name: string; 89 | type: string; 90 | shape: number[]; 91 | }[]; 92 | capabilities: string[]; 93 | modified_at: string; 94 | } 95 | 96 | type OllamaProcessesResponse = { 97 | models: ModelProcessInfo[]; 98 | } 99 | 100 | type ModelProcessInfo = { 101 | name: string; 102 | model: string; 103 | size: number; 104 | digest: string; 105 | details: { 106 | parent_model: string; 107 | format: string; 108 | family: string; 109 | families: string[] | null; 110 | parameter_size: string; 111 | quantization_level: string; 112 | }; 113 | expires_at: string; // ISO 8601 format 114 | size_vram: number; // No. of bytes used in memory 115 | } 116 | 117 | type OllamaPullResponseChunk = { 118 | status: string | 'success'; // Various status messages like 'pulling manifest', 'downloading x', but the final chunk should always be 'success' 119 | digest?: string; // Might not be present for first few chunks 120 | total?: number; // Total size of the model in bytes 121 | completed?: number; // Number of bytes downloaded so far, might not be present for first few chunks 122 | error?: string; // Error message if any 123 | } 124 | 125 | type OllamaChatRequest = { 126 | model: string; // The model name 127 | messages: OllamaMessage[]; // List of messages 128 | think?: boolean; // For thinking models only 129 | tools?: unknown[]; // TODO: add types for this / For tool call models only 130 | format?: unknown; // TODO: get from api 131 | options?: unknown; 132 | stream?: boolean; // Stream or not 133 | keep_alive?: string; // How long to keep model loaded in memory, default '5m'. 134 | options?: { 135 | num_ctx?: number; 136 | repeat_last_n?: number; 137 | repeat_penalty?: number; 138 | temperature?: number; 139 | seed?: number; 140 | stop?: string; 141 | num_predict?: number; 142 | top_k?: number; 143 | top_p?: number; 144 | min_p?: number; 145 | }; 146 | } -------------------------------------------------------------------------------- /src/types/toolsStore.d.ts: -------------------------------------------------------------------------------- 1 | type AppToolSchema = ({ 2 | name: string; 3 | type: 'string'; 4 | description?: string; 5 | enum?: string[]; 6 | } | { 7 | name: string; 8 | type: 'number'| 'integer'; 9 | description?: string; 10 | enum?: number[]; 11 | })[]; 12 | 13 | type AppTools = Record; 29 | -------------------------------------------------------------------------------- /src/types/types.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/ollama/ollama/blob/f9d2d8913554d78b1cae47c5eaa9cbbd0ea79273/docs/api.md#L502 2 | type MessageRole = 'system' | 'user' | 'assistant' | 'tool'; 3 | 4 | type OllamaToolCall = { 5 | function: { 6 | name: string; 7 | arguments: Record, 8 | } 9 | }; 10 | 11 | type OllamaMessage = { 12 | role: MessageRole; 13 | content: string; 14 | thinking?: string; // Thinking models' thinking text. 15 | images?: string[]; 16 | tool_calls?: OllamaToolCall[]; // List of tools the model wants to use 17 | } | { 18 | role: 'tool'; 19 | tool_name: string; 20 | content: string; 21 | } 22 | 23 | type ChatOld = { 24 | id: string; 25 | label: string; 26 | messages: AppMessage[]; 27 | created: number; // Date in ms 28 | lastMessage: number; // Date in ms 29 | pinned?: boolean; 30 | } 31 | 32 | interface AppMessage extends OllamaMessage { 33 | id: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/types/util.d.ts: -------------------------------------------------------------------------------- 1 | import type { Readable } from 'readable-stream'; 2 | 3 | type ReadableOf = Readable & AsyncIterable; -------------------------------------------------------------------------------- /src/utils/core/authedFetch.ts: -------------------------------------------------------------------------------- 1 | import supabase from '@/lib/supabase'; 2 | import { useConfigStore } from '@/stores/config'; 3 | 4 | export async function authedFetch(url: string, options?: RequestInit): Promise { 5 | if (!supabase) { 6 | return fetch(url, { 7 | ...options, 8 | }); 9 | } 10 | 11 | const session = (await supabase.auth.getSession()).data.session; 12 | 13 | const headers: Record = { 14 | ...(options?.headers || {}), 15 | }; 16 | 17 | // Only send auth token if api is explicitly enabled. 18 | if (useConfigStore().api.enabled) { 19 | headers['Authorization'] = `Bearer ${session?.access_token}`; 20 | } 21 | 22 | return fetch(url, { 23 | ...options, 24 | headers, 25 | }); 26 | } -------------------------------------------------------------------------------- /src/utils/core/filesAsBase64.ts: -------------------------------------------------------------------------------- 1 | function filesAsBase64(files: File[] | Blob[]): Promise { 2 | return Promise.all([...files].map(file => { 3 | return new Promise((resolve, reject) => { 4 | const reader = new FileReader(); 5 | 6 | reader.readAsDataURL(file); 7 | reader.onload = () => resolve(reader.result?.toString().split(',')[1] as string); 8 | reader.onerror = (error) => reject(error); 9 | }); 10 | })); 11 | } 12 | 13 | export { filesAsBase64 }; -------------------------------------------------------------------------------- /src/utils/core/getDateTimeString.ts: -------------------------------------------------------------------------------- 1 | export function getDateTimeString(time: Date | undefined) { 2 | if (time === undefined) return "Unknown"; 3 | 4 | return time.toLocaleString(undefined, { 5 | day: '2-digit', 6 | month: 'short', 7 | year: 'numeric', 8 | hour: '2-digit', 9 | minute: '2-digit', 10 | hour12: false, 11 | }); 12 | } -------------------------------------------------------------------------------- /src/utils/core/getMessageAttachments.ts: -------------------------------------------------------------------------------- 1 | import db from '@/lib/db'; 2 | 3 | export async function getMessageAttachmentBlobs(messageId: number): Promise { 4 | const results = await db.attachments 5 | .where('messageId') 6 | .equals(messageId) 7 | .toArray(); 8 | 9 | return results.map((item) => item.content); 10 | } -------------------------------------------------------------------------------- /src/utils/core/isDateBeforeToday.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the YYYY-MM-DD formatted `dateStr` (in UTC), is before today (in UTC). 3 | * @param dateStr The date in `YYYY-MM-DD` format. 4 | */ 5 | export default function isDateBeforeToday(dateStr: string): boolean { 6 | // Appending the time to make it midnight in UTC. 7 | const inputDate = new Date(dateStr + "T00:00:00Z"); 8 | 9 | // Get today’s UTC midnight timestamp 10 | const now = new Date(); 11 | const todayMidnightUTC = Date.UTC( 12 | now.getUTCFullYear(), 13 | now.getUTCMonth(), 14 | now.getUTCDate() 15 | ); 16 | 17 | return inputDate.getTime() < todayMidnightUTC; 18 | } -------------------------------------------------------------------------------- /src/utils/core/isOnMobile.ts: -------------------------------------------------------------------------------- 1 | export default function isOnMobile(): boolean { 2 | return ( 3 | typeof window !== 'undefined' && 4 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 5 | window.navigator.userAgent 6 | ) 7 | ); 8 | } -------------------------------------------------------------------------------- /src/utils/core/parseNumOrNull.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used for checking if a route id parameter is a valid number. 3 | * @param value The string/string list to parse. 4 | * @returns Parsed number or null if number could not be parsed. 5 | */ 6 | export default function parseNumOrNull(value: string | string[]): number | null { 7 | if (typeof value !== 'string') return null; 8 | const parsed = Number(value); 9 | return isNaN(parsed) ? null : parsed; 10 | } -------------------------------------------------------------------------------- /src/utils/core/promptDeleteChat.ts: -------------------------------------------------------------------------------- 1 | import router from "@/lib/router"; 2 | import useChatsStore from "@/stores/chatsStore"; 3 | 4 | /** 5 | * A small utility function so we keep the store seperate from any user/page-interaction logic. 6 | * @param chat Chat to delete. 7 | */ 8 | export function promptChatDeletion(chat: Chat) { 9 | if (confirm(`Are you sure you want to delete "${chat.title}"?`)) { 10 | useChatsStore().deleteChat(chat.id); 11 | router.push('/'); 12 | } 13 | } -------------------------------------------------------------------------------- /src/utils/core/setPageTitle.ts: -------------------------------------------------------------------------------- 1 | export default function setPageTitle(title: string) { 2 | document.title = `${title} | LlamaPen`; 3 | } -------------------------------------------------------------------------------- /src/utils/core/tryCatch.ts: -------------------------------------------------------------------------------- 1 | // Source: https://gist.github.com/t3dotgg/a486c4ae66d32bf17c09c73609dacc5b 2 | 3 | // Types for the result object with discriminated union 4 | type Success = { 5 | data: T; 6 | error: null; 7 | }; 8 | 9 | type Failure = { 10 | data: null; 11 | error: E; 12 | }; 13 | 14 | type Result = Success | Failure; 15 | 16 | // Main wrapper function 17 | export async function tryCatch( 18 | promise: Promise, 19 | ): Promise> { 20 | try { 21 | const data = await promise; 22 | return { data, error: null }; 23 | } catch (error) { 24 | return { data: null, error: error as E }; 25 | } 26 | } -------------------------------------------------------------------------------- /src/utils/ollamaRequest.ts: -------------------------------------------------------------------------------- 1 | import { useConfigStore } from "@/stores/config"; 2 | import { tryCatch } from "./core/tryCatch"; 3 | 4 | export default async function ollamaRequest( 5 | route: string, 6 | method: 'GET' | 'POST' | 'DELETE' = 'GET', 7 | body?: Record | Array | null | undefined, 8 | options?: { signal?: AbortSignal }, 9 | ) { 10 | return await tryCatch( 11 | fetch(useConfigStore().apiUrl(route), { 12 | method, 13 | body: body ? JSON.stringify(body) : null, 14 | signal: options?.signal, 15 | }) 16 | ); 17 | } -------------------------------------------------------------------------------- /src/utils/streamChunks.ts: -------------------------------------------------------------------------------- 1 | type Success = { chunk: T; error: null }; 2 | type Failure = { chunk: null; error: E }; 3 | type Result = Success | Failure; 4 | 5 | export async function* streamChunks( 6 | response: Response 7 | ): AsyncGenerator> { 8 | 9 | // Check if the response is ok and has a body 10 | // If not, yield an error and return 11 | if (!response.ok || !response.body) { 12 | yield { 13 | error: new Error(`HTTP Error: ${response.status} ${response.statusText}`), 14 | chunk: null, 15 | }; 16 | return; 17 | } 18 | 19 | const reader = response.body.getReader(); 20 | const decoder = new TextDecoder(); 21 | let buffer = ''; 22 | 23 | try { 24 | // Loop until done 25 | while (true) { 26 | const { done, value } = await reader.read(); 27 | if (done) break; 28 | 29 | if (value) { 30 | // Decode the value and append it to the buffer 31 | buffer += decoder.decode(value, { stream: true }); 32 | 33 | // Emit every complete line as a chunk 34 | let newlineIndex: number; 35 | while ((newlineIndex = buffer.indexOf('\n')) >= 0) { 36 | const line = buffer.slice(0, newlineIndex).trim(); 37 | buffer = buffer.slice(newlineIndex + 1); 38 | 39 | if (line) { 40 | try { 41 | const parsed = JSON.parse(line) as T; 42 | yield { chunk: parsed, error: null }; 43 | } catch (parseError) { 44 | yield { 45 | error: new Error(`JSON parse error: ${parseError} - Line: ${line}`), 46 | chunk: null, 47 | }; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | // Handle any remaining data in the buffer after the stream ends 55 | if (buffer.trim()) { 56 | try { 57 | const parsed = JSON.parse(buffer.trim()) as T; 58 | yield { chunk: parsed, error: null }; 59 | } catch (parseError) { 60 | yield { 61 | error: new Error(`JSON parse error at end of stream: ${parseError} - Remaining buffer: ${buffer}`), 62 | chunk: null, 63 | }; 64 | } 65 | } 66 | } catch (err) { 67 | // Handle any errors that occurred during reading the stream 68 | yield { error: err instanceof Error ? err : new Error(String(err)), chunk: null }; 69 | } finally { 70 | reader.releaseLock(); 71 | } 72 | } -------------------------------------------------------------------------------- /src/views/account/components/AccountSection.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/account/components/ContactSection.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/views/chat/ChatPage.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 71 | -------------------------------------------------------------------------------- /src/views/chat/components/ChatMessage/MessageInteractionButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/chat/components/ChatMessage/MessageModelSelectorItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/chat/components/ChatMessage/ModelMessage/ModelMessageHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/chat/components/ChatMessage/ModelToolCalls.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/views/chat/components/ChatMessage/ThinkBlock.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | -------------------------------------------------------------------------------- /src/views/chat/components/ChatMessage/ToolCallsMessage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/views/chat/components/GreetingText.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageEditor.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/InputElem.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImDarkTom/LlamaPen/b658130519c74a3197fe73e2650497d137f3a53f/src/views/chat/components/MessageInput/InputElem.vue -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/ScrollToBottomButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/buttons/FileUpload.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/buttons/MessageInputButton.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/buttons/MessageOption.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/buttons/MessageOptions.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/buttons/MessageTools.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageInput/buttons/ThinkingButton.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | -------------------------------------------------------------------------------- /src/views/chat/components/MessageList.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 124 | -------------------------------------------------------------------------------- /src/views/guide/GuidePage.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /src/views/models/ModelsPage.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | -------------------------------------------------------------------------------- /src/views/models/components/CapabilitiesSkeleton.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/views/models/components/InfoSection.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/models/components/ModelViewer.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | -------------------------------------------------------------------------------- /src/views/models/components/ModelsPageTypes.d.ts: -------------------------------------------------------------------------------- 1 | type ModelViewInfo = 2 | | { state: 'data'; model: OllamaModelInfoResponse; isLoaded: boolean } 3 | | { state: 'loading' } 4 | | { state: 'error'; message: string } 5 | | { state: 'unselected' }; 6 | 7 | type ModelSidebarListItem = { 8 | model: ModelListItem, 9 | loadedInMemory: boolean, 10 | }; -------------------------------------------------------------------------------- /src/views/models/components/ViewerContainer.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/views/models/todo.md: -------------------------------------------------------------------------------- 1 | 2 | # Feats to add 3 | 4 | - list model ✅ 5 | - show model information ✅ 6 | - copy a model ✅ 7 | - delete a model ✅ 8 | - pull a model ✅ 9 | - push a model (not planned) 10 | - generate embeddings (not planned) 11 | - list running models ✅ -------------------------------------------------------------------------------- /src/views/settings/components/CategoryLabel.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/views/settings/components/NumberInputSetting.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 73 | -------------------------------------------------------------------------------- /src/views/settings/components/OptionCategory.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/settings/components/OptionText.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/views/settings/components/SelectionSetting.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 47 | -------------------------------------------------------------------------------- /src/views/settings/components/StatusIndicator.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/settings/components/TextInputSetting.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 60 | -------------------------------------------------------------------------------- /src/views/settings/components/ToggleSetting.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /src/views/shortcuts/ShortcutsPage.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | -------------------------------------------------------------------------------- /src/views/shortcuts/components/ShortcutDisplay.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/views/tools/SelectInput.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/tools/TextInput.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/tools/ToolsPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/views/tools/edit/ToolRequestOptions.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './index.html', 5 | './src/**/*.{vue,js,ts,jsx,tsx}' 6 | ], 7 | theme: { 8 | extend: { 9 | typography: () => ({ 10 | DEFAULT: { 11 | css: { 12 | pre: false, 13 | code: false, 14 | 'pre code': false, 15 | 'code::before': false, 16 | 'code::after': false 17 | } 18 | }, 19 | app: { 20 | css: { 21 | '--tw-prose-body': 'var(--color-text-muted)', 22 | '--tw-prose-headings': 'var(--color-text)', 23 | '--tw-prose-lead': 'var(--color-text)', 24 | '--tw-prose-links': 'var(--color-secondary)', 25 | '--tw-prose-bold': 'var(--color-text-muted)', 26 | '--tw-prose-counters': 'var(--color-primary)', 27 | '--tw-prose-bullets': 'var(--color-primary)', 28 | '--tw-prose-hr': 'var(--color-text-muted)', 29 | '--tw-prose-quotes': 'var(--color-text-muted)', 30 | '--tw-prose-quote-borders': 'var(--color-text-muted)', 31 | '--tw-prose-captions': 'var(--color-text-muted)', 32 | '--tw-prose-code': 'var(--color-text-muted)', 33 | '--tw-prose-pre-code': 'var(--color-text-muted)', 34 | '--tw-prose-pre-bg': 'var(--color-text-muted)', 35 | '--tw-prose-th-borders': 'var(--color-text-muted)', 36 | '--tw-prose-td-borders': 'var(--color-text-muted)', 37 | }, 38 | }, 39 | }), 40 | } 41 | }, 42 | plugins: [require("@tailwindcss/typography"), require('tailwindcss-motion')], 43 | } -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | 6 | /* Linting */ 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "baseUrl": "./", 12 | "paths": { 13 | "@/*": ["./src/*"] 14 | }, 15 | "noUncheckedSideEffectImports": true 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true, 22 | 23 | /* Custom */ 24 | "noImplicitAny": true, 25 | "strictNullChecks": true, 26 | "strictFunctionTypes": true, 27 | "strictBindCallApply": true, 28 | "strictPropertyInitialization": true, 29 | "noImplicitThis": true, 30 | "alwaysStrict": true 31 | }, 32 | "include": ["vite.config.ts"] 33 | } 34 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "rewrites": [ 4 | { 5 | "source": "/robots.txt", 6 | "destination": "/robots.txt" 7 | }, 8 | { 9 | "source": "/sitemap.xml", 10 | "destination": "/sitemap.xml" 11 | }, 12 | { 13 | "source": "/(.*)", 14 | "destination": "/" 15 | } 16 | ], 17 | "trailingSlash": false 18 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import path from "node:path"; 4 | import tailwindcss from "@tailwindcss/vite"; 5 | import svgLoader from 'vite-svg-loader'; 6 | import { VitePWA } from 'vite-plugin-pwa'; 7 | import { execSync } from "node:child_process"; 8 | 9 | const commitHash = execSync('git rev-parse HEAD').toString().trim(); 10 | 11 | // https://vite.dev/config/ 12 | export default defineConfig({ 13 | define: { 14 | __COMMIT_HASH__: JSON.stringify(commitHash), 15 | __APP_VERSION__: JSON.stringify(process.env.npm_package_version), 16 | }, 17 | plugins: [ 18 | vue(), 19 | tailwindcss(), 20 | svgLoader(), 21 | VitePWA({ 22 | manifest: { 23 | name: 'LlamaPen', 24 | short_name: 'LlamaPen', 25 | description: 'A no-install needed GUI for Ollama.', 26 | theme_color: '#020726', 27 | background_color: '#000019', 28 | display: 'standalone', 29 | start_url: '/', 30 | icons: [ 31 | { 32 | src: "/icons/logo-512.png", 33 | sizes: "512x512", 34 | type: "image/png", 35 | purpose: "any maskable" 36 | }, 37 | { 38 | src: "/icons/logo-256.png", 39 | sizes: "256x256", 40 | type: "image/png", 41 | purpose: "any maskable" 42 | }, 43 | { 44 | src: "/icons/logo-192.png", 45 | sizes: "192x192", 46 | type: "image/png", 47 | purpose: "any maskable" 48 | }, 49 | { 50 | src: "/icons/logo-180.png", 51 | sizes: "180x180", 52 | type: "image/png", 53 | purpose: "any maskable" 54 | }, 55 | { 56 | src: "/icons/logo-152.png", 57 | sizes: "152x152", 58 | type: "image/png", 59 | purpose: "any maskable" 60 | }, 61 | { 62 | src: "/icons/logo-120.png", 63 | sizes: "120x120", 64 | type: "image/png", 65 | purpose: "any maskable" 66 | }, 67 | { 68 | src: "/icons/logo-64.png", 69 | sizes: "64x64", 70 | type: "image/png", 71 | purpose: "any maskable" 72 | }, 73 | { 74 | src: "/icons/logo-48.png", 75 | sizes: "48x48", 76 | type: "image/png", 77 | purpose: "any maskable" 78 | }, 79 | { 80 | src: "/icons/logo-32.png", 81 | sizes: "32x32", 82 | type: "image/png", 83 | purpose: "any maskable" 84 | }, 85 | { 86 | src: "/icons/logo-16.png", 87 | sizes: "16x16", 88 | type: "image/png", 89 | purpose: "any maskable" 90 | } 91 | ] 92 | }, 93 | registerType: 'autoUpdate', 94 | workbox: { 95 | globPatterns: ['**/*.{js,css,html,ico,png,svg,txt,xml}'] 96 | }, 97 | devOptions: { 98 | enabled: true 99 | }, 100 | }), 101 | ], 102 | resolve: { 103 | alias: { 104 | "@": path.resolve(__dirname, "src"), 105 | } 106 | }, 107 | }); 108 | --------------------------------------------------------------------------------