├── .github ├── actions │ └── publish-artifacts │ │ └── action.yml └── workflows │ └── build-and-deploy.yml ├── .gitignore ├── README.md ├── components.json ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── tauri.svg └── vite.svg ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── lib.rs │ └── main.rs └── tauri.conf.json ├── src ├── app │ ├── index.tsx │ └── parts │ │ ├── chat-header.tsx │ │ ├── chat-message.tsx │ │ ├── chat-window.tsx │ │ ├── settings-wrapper.tsx │ │ └── sidebar.tsx ├── assets │ └── react.svg ├── components │ └── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ └── select.tsx ├── core │ ├── actions.ts │ ├── core.ts │ ├── database-actions.ts │ ├── helper.ts │ ├── index.ts │ ├── local-database.ts │ ├── types.ts │ └── utils.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/actions/publish-artifacts/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish artifacts 2 | description: Publishes artifacts after CI process 3 | inputs: 4 | target: 5 | description: target triples for built artifact 6 | profile: 7 | description: "'debug' or 'release'" 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Determine short GitHub SHA 12 | shell: bash 13 | run: | 14 | export GITHUB_SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA") 15 | echo "GITHUB_SHA_SHORT=$GITHUB_SHA_SHORT" >> $GITHUB_ENV 16 | 17 | - name: Publish artifacts (Linux - AppImage) 18 | if: ${{ matrix.settings.host == 'ubuntu-20.04' }} 19 | uses: actions/upload-artifact@v3 20 | with: 21 | name: Ollama-AppImage-${{ inputs.target }}-${{ env.GITHUB_SHA_SHORT }} 22 | path: target/${{ inputs.target }}/${{ inputs.profile }}/bundle/appimage/*.AppImage 23 | if-no-files-found: error 24 | retention-days: 1 25 | 26 | - name: Publish artifacts (Windows - msi) 27 | if: ${{ matrix.settings.host == 'windows-latest' }} 28 | uses: actions/upload-artifact@v3 29 | with: 30 | name: Ollama-Windows-msi-${{ inputs.target }}-${{ env.GITHUB_SHA_SHORT }} 31 | path: target/${{ inputs.target }}/${{ inputs.profile }}/bundle/msi/*.msi 32 | if-no-files-found: error 33 | retention-days: 1 34 | 35 | - name: Publish artifacts (macOS - dmg) 36 | if: ${{ matrix.settings.host == 'macos-latest' }} 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: Ollama-macOS-dmg-${{ inputs.target }}-${{ env.GITHUB_SHA_SHORT }} 40 | path: target/${{ inputs.target }}/${{ inputs.profile }}/bundle/dmg/*.dmg 41 | if-no-files-found: error 42 | retention-days: 1 43 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | # Name of the GitHub Actions workflow 2 | name: Build and Deploy 3 | 4 | # Trigger the workflow on push to the specified branch 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | # Define the jobs to be run 11 | jobs: 12 | # Define the build job 13 | build: 14 | # Specify the runner environment 15 | runs-on: macos-latest 16 | 17 | # Define the steps to be performed in the build job 18 | steps: 19 | # Add the target architecture for building MacOS Silicon apps 20 | - name: Add target architecture 21 | run: rustup target add aarch64-apple-darwin 22 | 23 | # Check out the repository code to the runner 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | # Set up the required Node.js version 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: "20.x" 32 | 33 | # Set up the Rust toolchain 34 | - name: Setup Rust 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | profile: minimal 38 | toolchain: stable 39 | override: true 40 | 41 | # Check if there is a cache of the pnpm modules and restore it 42 | - name: Cache pnpm modules 43 | uses: actions/cache@v2 44 | with: 45 | path: ~/.pnpm-store 46 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 47 | restore-keys: | 48 | ${{ runner.os }}-pnpm- 49 | 50 | # Install pnpm (a fast, disk space efficient package manager) 51 | - name: Install pnpm 52 | run: npm install -g pnpm 53 | 54 | # Install project dependencies 55 | - name: Install dependencies 56 | run: pnpm i 57 | 58 | # Build the project for MacOS Silicon 59 | - name: Build for MacOS Silicon 60 | run: pnpm build:app:silicon 61 | 62 | # Archive the MacOS Silicon build artifacts 63 | - name: Archive MacOS Silicon artifacts 64 | uses: actions/upload-artifact@v3 65 | with: 66 | name: macos-silicon 67 | # The current path is very specific and might need adjustments with any changes in the file name or structure. A more generalized path (e.g., src-tauri/target/release/bundle/dmg/*.dmg) would be preferable as it's more flexible and resilient to such changes. 68 | path: src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg 69 | if-no-files-found: error # or 'warn' or 'ignore' 70 | 71 | # Pre command for intell build 72 | - name: rustup command 73 | run: rustup target add x86_64-apple-darwin 74 | 75 | # Build the project for MacOS Intel 76 | - name: Build for MacOS Intel 77 | run: pnpm build:app:intell 78 | 79 | # Archive the MacOS Intel build artifacts 80 | - name: Archive MacOS Intel artifacts 81 | uses: actions/upload-artifact@v3 82 | with: 83 | name: macos-intel 84 | # The current path is very specific and might need adjustments with any changes in the file name or structure. A more generalized path (e.g., src-tauri/target/release/bundle/dmg/*.dmg) would be preferable as it's more flexible and resilient to such changes. 85 | path: src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg 86 | if-no-files-found: error # or 'warn' or 'ignore' 87 | 88 | # Build the project for MacOS Universal (both Silicon and Intel) 89 | - name: Build for MacOS Universal 90 | run: pnpm build:app:universal 91 | 92 | # Archive the MacOS Universal build artifacts 93 | - name: Archive MacOS Universal artifacts 94 | uses: actions/upload-artifact@v3 95 | with: 96 | name: macos-universal 97 | # The current path is very specific and might need adjustments with any changes in the file name or structure. A more generalized path (e.g., src-tauri/target/release/bundle/dmg/*.dmg) would be preferable as it's more flexible and resilient to such changes. 98 | path: src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg 99 | if-no-files-found: error # or 'warn' or 'ignore' 100 | 101 | # Placeholder steps for future implementation for Windows 102 | # Build the project for Windows OS 103 | 104 | - name: Pre windows script 105 | run: rustup target add x86_64-pc-windows-msvc 106 | 107 | 108 | - name: Build for Windows 109 | run: pnpm build:app:windows 110 | 111 | - name: Archive Windows artifacts 112 | uses: actions/upload-artifact@v3 113 | with: 114 | name: windows 115 | path: src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe 116 | 117 | # Placeholder steps for future implementation for Linux 118 | # Build the project for Linux OS 119 | # - name: Build for Linux 120 | # run: pnpm build:app:linux 121 | 122 | # - name: Archive Linux artifacts 123 | # uses: actions/upload-artifact@v3 124 | # with: 125 | # name: linux 126 | # path: src-tauri/target/release/bundle/linux/* # Adjust this path if necessary 127 | 128 | # # List the contents of the build directory 129 | # # For Debugging purposes 130 | # - name: List files in the build directory 131 | # run: | 132 | # echo "Listing contents of macos-silicon directory" 133 | # ls -la src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/ 134 | # echo "Listing contents of macos-intel directory" 135 | # ls -la src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/ 136 | # echo "Listing contents of macos-universal directory" 137 | # ls -la src-tauri/target/universal-apple-darwin/release/bundle/dmg/ 138 | 139 | # Define the release job which depends on the build job 140 | release: 141 | # Specify that this job needs the build job to complete successfully 142 | needs: build 143 | # Specify the runner environment 144 | runs-on: ubuntu-latest 145 | 146 | # Define the steps to be performed in the release job 147 | steps: 148 | # Download the build artifacts from the build job 149 | - name: Download artifacts 150 | uses: actions/download-artifact@v3 151 | 152 | # # List the contents of the directories 153 | # # For debugging purposes 154 | # - name: List contents of directories 155 | # run: | 156 | # echo "Listing contents of macos-silicon directory" 157 | # ls -la /home/runner/work/Ollama-Gui/Ollama-Gui/macos-silicon 158 | # echo "Listing contents of macos-intel directory" 159 | # ls -la /home/runner/work/Ollama-Gui/Ollama-Gui/macos-intel 160 | # echo "Listing contents of macos-universal directory" 161 | # ls -la /home/runner/work/Ollama-Gui/Ollama-Gui/macos-universal 162 | 163 | # Needs to be updated to use an adequate generated tag 164 | # Generate the tag 165 | - name: Generate tag 166 | id: generate_tag 167 | run: echo "::set-output name=tag::release-$(date +'%Y%m%d%H%M%S')" 168 | # Create a new GitHub release with the generated tag 169 | - name: Create GitHub Release 170 | id: create_release 171 | uses: actions/create-release@v1 172 | env: 173 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | with: 175 | tag_name: ${{ steps.generate_tag.outputs.tag }} 176 | release_name: Release ${{ steps.generate_tag.outputs.tag }} 177 | draft: false 178 | prerelease: false 179 | 180 | # Get the file name and assign it to an object 181 | - name: Get Silicon file name 182 | id: get_filename_silicon 183 | run: echo "::set-output name=filename::$(ls /home/runner/work/Ollama-Gui/Ollama-Gui/macos-silicon/*.dmg)" 184 | 185 | # Print the file name 186 | - name: Echo the Silicon file name 187 | run: echo "The file name is ${{ steps.get_filename_silicon.outputs.filename }}" 188 | 189 | # Upload the MacOS Silicon build artifact to the GitHub release 190 | - name: Upload Release Asset (MacOS Silicon) 191 | uses: actions/upload-release-asset@v1 192 | env: 193 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 194 | with: 195 | upload_url: ${{ steps.create_release.outputs.upload_url }} 196 | asset_path: ${{ steps.get_filename_silicon.outputs.filename }} # adjusted path 197 | asset_name: Ollama-Gui-MacOS-Silicon.dmg 198 | asset_content_type: application/octet-stream 199 | 200 | # Get the file name and assign it to an object 201 | - name: Get Intel file name 202 | id: get_filename_intel 203 | run: echo "::set-output name=filename::$(ls /home/runner/work/Ollama-Gui/Ollama-Gui/macos-intel/*.dmg)" 204 | 205 | # Print the file name 206 | - name: Echo the Intel file name 207 | run: echo "The Intel file name is ${{ steps.get_filename_intel.outputs.filename }}" 208 | 209 | # Upload the MacOS Intel build artifact to the GitHub release 210 | - name: Upload Release Asset (MacOS Intel) 211 | uses: actions/upload-release-asset@v1 212 | env: 213 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 214 | with: 215 | upload_url: ${{ steps.create_release.outputs.upload_url }} 216 | asset_path: ${{ steps.get_filename_intel.outputs.filename }} # adjusted path 217 | asset_name: Ollama-Gui-MacOS-Intel.dmg 218 | asset_content_type: application/octet-stream 219 | 220 | # Get the file name and assign it to an object 221 | - name: Get Universal file name 222 | id: get_filename_universal 223 | run: echo "::set-output name=filename::$(ls /home/runner/work/Ollama-Gui/Ollama-Gui/macos-universal/*.dmg)" 224 | 225 | # Print the file name 226 | - name: Echo the Universal file name 227 | run: echo "The Universal file name is ${{ steps.get_filename_universal.outputs.filename }}" 228 | 229 | # Upload the MacOS Universal build artifact to the GitHub release 230 | - name: Upload Release Asset (MacOS Universal) 231 | uses: actions/upload-release-asset@v1 232 | env: 233 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 234 | with: 235 | upload_url: ${{ steps.create_release.outputs.upload_url }} 236 | asset_path: ${{ steps.get_filename_universal.outputs.filename }} # adjusted path 237 | asset_name: Ollama-Gui-MacOS-Universal.dmg 238 | asset_content_type: application/octet-stream 239 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ollama Chat App 🐐 V2 2 | 3 | 4 | 5 | This is a re write of the first version of Ollama chat, The new update will include some time saving features and make it more stable and available for Macos and Windows. Also a new freshly look will be included as well. 6 |
7 | 8 | Stay in touch for upcoming updates 9 | 10 |
11 | 12 | Todo list: 13 | 14 | - Add server auto start 15 | - Add dark mode 16 | - fix some minor bugs 17 | - Improve settings page 18 | 19 |
20 |
21 | 22 | Welcome to my Ollama Chat, this is an interface for the Official ollama CLI to make it easier to chat. It includes futures such as: 23 | 24 | - Improved interface design & user friendly 25 | - ~~Auto check if ollama is running~~ _(**NEW**, Auto start ollama server)_ ⏰ 26 | - Multiple conversations 💬 27 | - Detect which models are available to use 📋 28 | - Able to change the host where ollama is running at 🖥️ 29 | - Perstistance 📀 30 | - Import & Export Chats 🚛 31 | - Light & Dark Theme 🌗 32 | 33 | For any questions, please contact [Twan Luttik (Twitter - X)](https://twitter.com/twanluttik) 34 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + React + Typescript 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ollama-chat", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "clean-target": "rm -rf ./src-tauri/target", 8 | "build:app:windows": "tauri build --target x86_64-pc-windows-msvc", 9 | "build:app:silicon": "pnpm clean-target && tauri build --target aarch64-apple-darwin", 10 | "build:app:intell": "pnpm clean-target && tauri build --target x86_64-apple-darwin", 11 | "build:app:universal": "pnpm clean-target && tauri build --target universal-apple-darwin", 12 | "dev": "vite", 13 | "build": "tsc && vite build", 14 | "preview": "vite preview", 15 | "tauri": "tauri" 16 | }, 17 | "dependencies": { 18 | "@radix-ui/react-dialog": "^1.1.1", 19 | "@radix-ui/react-icons": "^1.3.0", 20 | "@radix-ui/react-select": "^2.1.1", 21 | "@radix-ui/react-slot": "^1.1.0", 22 | "@tauri-apps/api": ">=2.0.0-rc.0", 23 | "@tauri-apps/plugin-fs": "2.0.0-rc.0", 24 | "@tauri-apps/plugin-shell": "2.0.0-rc.0", 25 | "@tauri-apps/plugin-sql": "2.0.0-rc.0", 26 | "axios": "^1.7.3", 27 | "better-sqlite3": "^11.1.2", 28 | "class-variance-authority": "^0.7.0", 29 | "clsx": "^2.1.1", 30 | "dayjs": "^1.11.12", 31 | "immer": "^10.1.1", 32 | "marked": "^14.0.0", 33 | "react": "^18.2.0", 34 | "react-dom": "^18.2.0", 35 | "react-hook-form": "^7.52.2", 36 | "simple-core-state": "^0.0.20", 37 | "tailwind-merge": "^2.4.0", 38 | "tailwindcss-animate": "^1.0.7", 39 | "util.promisify": "^1.1.2" 40 | }, 41 | "devDependencies": { 42 | "@tauri-apps/cli": ">=2.0.0-rc.0", 43 | "@types/better-sqlite3": "^7.6.11", 44 | "@types/node": "^22.2.0", 45 | "@types/react": "^18.2.15", 46 | "@types/react-dom": "^18.2.7", 47 | "@vitejs/plugin-react": "^4.2.1", 48 | "autoprefixer": "^10.4.20", 49 | "postcss": "^8.4.41", 50 | "tailwindcss": "^3.4.9", 51 | "typescript": "^5.2.2", 52 | "vite": "^5.3.1" 53 | } 54 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ollama-chat" 3 | version = "0.0.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | name = "ollama_chat_lib" 12 | crate-type = ["lib", "cdylib", "staticlib"] 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "2.0.0-rc", features = [] } 16 | 17 | [dependencies] 18 | tauri = { version = "2.0.0-rc", features = [] } 19 | tauri-plugin-shell = "2.0.0-rc" 20 | serde = { version = "1", features = ["derive"] } 21 | serde_json = "1" 22 | tauri-plugin-fs = "2.0.0-rc.0" 23 | 24 | 25 | [dependencies.tauri-plugin-sql] 26 | features = ["sqlite"] 27 | version = "2.0.0-rc" 28 | 29 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "sql:default", 10 | "sql:allow-select", 11 | "sql:allow-load", 12 | "sql:allow-execute", 13 | "core:app:default", 14 | "shell:allow-open", 15 | "shell:default", 16 | { 17 | "identifier": "shell:allow-execute", 18 | "allow": [ 19 | { 20 | "name": "ollama-server", 21 | "cmd": "ollama", 22 | "args": [ 23 | "serve" 24 | ], 25 | "sidecar": false 26 | } 27 | ] 28 | }, 29 | "fs:default", 30 | { 31 | "identifier": "fs:allow-exists", 32 | "allow": [ 33 | { 34 | "path": "*" 35 | } 36 | ] 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollama-interface/Ollama-Gui/a33c96c1015535dec2afd916f6e5f9dcdecc478b/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command 2 | #[tauri::command] 3 | fn greet(name: &str) -> String { 4 | format!("Hello, {}! You've been greeted from Rust!", name) 5 | } 6 | 7 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 8 | pub fn run() { 9 | tauri::Builder::default() 10 | .plugin(tauri_plugin_fs::init()) 11 | .plugin(tauri_plugin_shell::init()) 12 | .plugin(tauri_plugin_sql::Builder::default().build()) 13 | .invoke_handler(tauri::generate_handler![greet]) 14 | .run(tauri::generate_context!()) 15 | .expect("error while running tauri application"); 16 | } 17 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | ollama_chat_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "ollama-chat", 3 | "version": "0.0.0", 4 | "identifier": "com.twanluttik.ollama-interface", 5 | "build": { 6 | "beforeDevCommand": "pnpm dev", 7 | "devUrl": "http://localhost:1420", 8 | "beforeBuildCommand": "pnpm build", 9 | "frontendDist": "../dist" 10 | }, 11 | "app": { 12 | "windows": [ 13 | { 14 | "title": "Ollama Chat", 15 | "width": 1050, 16 | "height": 750 17 | } 18 | ], 19 | "security": { 20 | "csp": null 21 | } 22 | }, 23 | "bundle": { 24 | "android": { 25 | "minSdkVersion": 24 26 | }, 27 | "macOS": { 28 | "dmg": { 29 | "appPosition": { 30 | "x": 180, 31 | "y": 170 32 | }, 33 | "applicationFolderPosition": { 34 | "x": 480, 35 | "y": 170 36 | }, 37 | "windowSize": { 38 | "height": 400, 39 | "width": 660 40 | } 41 | }, 42 | "files": {}, 43 | "hardenedRuntime": true, 44 | "minimumSystemVersion": "10.13" 45 | }, 46 | "active": true, 47 | "targets": "all", 48 | "icon": [ 49 | "icons/32x32.png", 50 | "icons/128x128.png", 51 | "icons/128x128@2x.png", 52 | "icons/icon.icns", 53 | "icons/icon.ico" 54 | ] 55 | }, 56 | "plugins": { 57 | "node": { 58 | "enabled": true 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { actions, core, syncModels } from "@/core"; 3 | import { loadDB } from "@/core/local-database"; 4 | import { Sidebar } from "@/app/parts/sidebar"; 5 | import { ChatWindow } from "./parts/chat-window"; 6 | import { SettingsWrapper } from "./parts/settings-wrapper"; 7 | 8 | // Load the database on the app frame 9 | loadDB(); 10 | 11 | export const AppFrame = () => { 12 | // async function startServer() { 13 | // let result = await Command.create("ollama-server", [ 14 | // "-c", 15 | // "OLLAMA_ORIGINS=* OLLAMA_HOST=127.0.0.1:11434 ollama serve", 16 | // ]).execute(); 17 | // console.log(result); 18 | // } 19 | 20 | const loadAppData = async () => { 21 | // Load available models 22 | syncModels(); 23 | 24 | // Get all conversations from the db 25 | const res = await actions.getConversations(); 26 | core.conversations.set(res as any); 27 | }; 28 | 29 | useEffect(() => { 30 | // Load app data in order for functionality 31 | loadAppData(); 32 | }, []); 33 | 34 | return ( 35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/parts/chat-header.tsx: -------------------------------------------------------------------------------- 1 | import { core } from "@/core"; 2 | import dayjs from "dayjs"; 3 | import { useSimple } from "simple-core-state"; 4 | 5 | export const ChatHeader = () => { 6 | const conversation_meta = useSimple(core.focused_conv_meta); 7 | return ( 8 |
12 |
13 |
14 |

15 | {conversation_meta?.title || " "} 16 |

17 |

{conversation_meta.model}

18 |
19 | {conversation_meta.created_at && ( 20 |
21 |

Created:

22 |

23 | {dayjs(conversation_meta.created_at).format( 24 | "MMM DD YYYY, HH:mm:ss a" 25 | )} 26 |

27 |
28 | )} 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/parts/chat-message.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | import { ConversationMessage } from "@/core/types"; 3 | import { marked } from "marked"; 4 | import { twMerge } from "tailwind-merge"; 5 | 6 | interface ChatMessageProps extends ConversationMessage {} 7 | export const ChatMessage = (props: ChatMessageProps) => { 8 | const contentRef = useRef(null); 9 | 10 | const parseText = useCallback(async () => { 11 | const p = await marked.parse(props.message); 12 | if (contentRef?.current) contentRef.current.innerHTML = p; 13 | }, [props.message]); 14 | 15 | useEffect(() => { 16 | parseText(); 17 | }, []); 18 | 19 | return ( 20 |
26 | {props?.ai_replied ? ( 27 |
28 |

From: AI

29 |
30 | ) : ( 31 | <> 32 | )} 33 |
39 |
46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/app/parts/chat-window.tsx: -------------------------------------------------------------------------------- 1 | import { ChatHeader } from "./chat-header"; 2 | import { Input } from "@/components/ui/input"; 3 | import { useCallback, useState } from "react"; 4 | import { 5 | actions, 6 | core, 7 | generateIdNumber, 8 | generateRandomId, 9 | sendPrompt, 10 | } from "@/core"; 11 | import { useSimple } from "simple-core-state"; 12 | import dayjs from "dayjs"; 13 | import { produce } from "immer"; 14 | import { ConversationMessage, ConversationMeta } from "@/core/types"; 15 | import { ChatMessage } from "./chat-message"; 16 | import { Button } from "@/components/ui/button"; 17 | import { 18 | Select, 19 | SelectContent, 20 | SelectItem, 21 | SelectTrigger, 22 | SelectValue, 23 | } from "@/components/ui/select"; 24 | 25 | export const ChatWindow = () => { 26 | const conversations = useSimple(core.conversations); 27 | const conv_id = useSimple(core.focused_conv_id); 28 | const messages = useSimple(core.focused_conv_data); 29 | const conversation_meta = useSimple(core.focused_conv_meta); 30 | const available_models = useSimple(core.available_models); 31 | const last_used_model = useSimple(core.last_used_model); 32 | const [msg, setMsg] = useState(""); 33 | const [loading, setLoading] = useState(false); 34 | 35 | const messagesList = useCallback(() => { 36 | return messages; 37 | }, [messages, conv_id]); 38 | 39 | // TODO: We need to move this function to a life cycle for auto restart feature 40 | // async function startServer() { 41 | // let result = await Command.create("ollama-server", [ 42 | // "-c", 43 | // "OLLAMA_ORIGINS=* OLLAMA_HOST=127.0.0.1:11434 ollama serve", 44 | // ]).execute(); 45 | // console.log(result); 46 | // } 47 | 48 | const changeModel = (model_name: string) => { 49 | // Update last used 50 | core.last_used_model.set(model_name); 51 | 52 | // Update the current conversation we are looking at 53 | core.focused_conv_meta.patchObject({ model: model_name }); 54 | }; 55 | 56 | const sendPromptMessage = useCallback(async () => { 57 | setLoading(true); 58 | 59 | // Check if we need to create a new conversation first 60 | if (!conv_id) { 61 | const v = { 62 | id: generateRandomId(12), 63 | created_at: dayjs().toDate(), 64 | model: last_used_model, 65 | title: "Conversation " + generateIdNumber(2), 66 | }; 67 | 68 | actions.createConversation(v); 69 | 70 | core.conversations.set( 71 | produce((draft) => { 72 | draft.push(v as unknown as ConversationMeta); 73 | }) 74 | ); 75 | 76 | core.focused_conv_id.set(v.id); 77 | core.focused_conv_meta.set(v); 78 | core.focused_conv_data.set([]); 79 | } 80 | 81 | let m = msg; 82 | let _conversation_id = conv_id; 83 | setMsg(""); 84 | 85 | const v1 = { 86 | id: generateRandomId(12), 87 | conversation_id: _conversation_id, 88 | message: m, 89 | created_at: dayjs().toDate(), 90 | ai_replied: false, 91 | ctx: "", 92 | }; 93 | 94 | // save the send prompt in db 95 | await actions.sendPrompt(v1); 96 | 97 | // Update the local state 98 | const messageCopy = [...messages]; 99 | core.focused_conv_data.set( 100 | produce(messageCopy, (draft) => { 101 | draft.push(v1 as unknown as ConversationMessage); 102 | }) 103 | ); 104 | 105 | let lastCtx = []; 106 | if (messages.length > 1) { 107 | lastCtx = JSON.parse((messages[1].ctx as string) || ""); 108 | } 109 | 110 | if (messages?.length === 0) { 111 | const x = msg?.slice(0, 20); 112 | core.focused_conv_meta.updatePiece("title", x); 113 | actions.updateConversationName(x, conversation_meta.id); 114 | } 115 | 116 | // send the promp the the ai 117 | const res = await sendPrompt({ 118 | model: conversation_meta.model, 119 | prompt: m, 120 | context: lastCtx, 121 | }); 122 | // TODO: we need to handle network error or any other... 123 | 124 | const v2 = { 125 | ai_replied: true, 126 | conversation_id: _conversation_id, 127 | created_at: dayjs().toDate(), 128 | id: generateRandomId(12), 129 | message: res.response, 130 | ctx: res.context, 131 | }; 132 | 133 | // save the send prompt in db 134 | await actions.sendPrompt(v2); 135 | 136 | // Update the local state 137 | const messageCopy2 = [...messages]; 138 | core.focused_conv_data.set( 139 | produce(messageCopy2, (draft) => { 140 | draft.push(v2 as unknown as ConversationMessage); 141 | }) 142 | ); 143 | 144 | setLoading(false); 145 | }, [ 146 | msg, 147 | messages, 148 | conversations, 149 | last_used_model, 150 | conversation_meta, 151 | conv_id, 152 | ]); 153 | 154 | return ( 155 |
156 | 157 |
161 |
162 | {messages.length === 0 && ( 163 |
164 |

Select model:

165 | 182 |
183 | )} 184 | {(messagesList() || []).map((item) => ( 185 | 186 | ))} 187 |
188 |
189 | setMsg(x.target.value)} 194 | value={msg} 195 | onKeyDown={(x) => { 196 | if (x.code === "Enter") { 197 | sendPromptMessage(); 198 | } 199 | }} 200 | /> 201 | 209 |
210 |
211 |
212 | ); 213 | }; 214 | -------------------------------------------------------------------------------- /src/app/parts/settings-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Input } from "@/components/ui/input"; 3 | import { actions, core } from "@/core"; 4 | import { ReactNode, useState } from "react"; 5 | import { Controller, SubmitHandler, useForm } from "react-hook-form"; 6 | import { useSimple, useSimpleEvent } from "simple-core-state"; 7 | 8 | type Inputs = { 9 | host: string; 10 | }; 11 | 12 | export const SettingsWrapper = ({ children }: { children: ReactNode }) => { 13 | const host_url = useSimple(core.server_host); 14 | const [open, setOpen] = useState(false); 15 | useSimpleEvent(core._events.trigger_settings, () => { 16 | setOpen(true); 17 | }); 18 | 19 | const { 20 | control, 21 | reset, 22 | handleSubmit, 23 | formState: { isDirty }, 24 | } = useForm({ values: { host: host_url } }); 25 | const onSubmit: SubmitHandler = (data) => console.log(data); 26 | 27 | const clearDatabase = async () => { 28 | await actions.flushDatbase(); 29 | core.conversations.reset(); 30 | core.focused_conv_data.reset(); 31 | core.focused_conv_id.reset(); 32 | core.focused_conv_meta.reset(); 33 | }; 34 | 35 | return ( 36 |
37 | {open && ( 38 |
39 |
40 |
41 |

Settings

42 | 50 |
51 |
52 | {isDirty && ( 53 |
54 | 57 | 64 |
65 | )} 66 | 67 |
68 |

Ollama remote address

69 | ( 74 | 75 | )} 76 | /> 77 |
78 |
79 |
80 |

Clear all conversations and messages

81 | 84 |
85 |
86 |

Build by:

87 | 93 | 94 | 102 |
103 |
104 |
105 | )} 106 | {children} 107 |
108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /src/app/parts/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { actions, core, generateIdNumber, generateRandomId } from "@/core"; 3 | import { ConversationMeta } from "@/core/types"; 4 | import { GearIcon } from "@radix-ui/react-icons"; 5 | import dayjs from "dayjs"; 6 | import { produce } from "immer"; 7 | import { useSimple } from "simple-core-state"; 8 | import { twMerge } from "tailwind-merge"; 9 | 10 | export const Sidebar = () => { 11 | const convs = useSimple(core.conversations); 12 | const focused_conv_id = useSimple(core.focused_conv_id); 13 | const last_used_model = useSimple(core.last_used_model); 14 | 15 | const newConversation = () => { 16 | const v = { 17 | id: generateRandomId(12), 18 | created_at: dayjs().toDate(), 19 | model: last_used_model, 20 | title: "Conversation " + generateIdNumber(2), 21 | }; 22 | actions.createConversation(v); 23 | 24 | core.conversations.set( 25 | produce((draft) => { 26 | draft.push(v as unknown as ConversationMeta); 27 | }) 28 | ); 29 | 30 | core.focused_conv_id.set(v.id); 31 | core.focused_conv_meta.set(v); 32 | core.focused_conv_data.set([]); 33 | }; 34 | 35 | const loadConversation = async (conv: ConversationMeta) => { 36 | // set data 37 | core.focused_conv_id.set(conv.id); 38 | core.focused_conv_meta.set(conv); 39 | 40 | // Get messages from the conversation 41 | const res = await actions.getConversationMessages(conv.id); 42 | 43 | core.focused_conv_data.set(res as any); 44 | }; 45 | 46 | return ( 47 |
48 |
49 | 52 | 59 |
60 |
61 |
62 | {!!convs?.length && 63 | convs?.map((item, index) => ( 64 |
loadConversation(item)} 71 | > 72 |

{item.title}

73 |
74 | ))} 75 |
76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { Cross2Icon } from "@radix-ui/react-icons" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )) 52 | DialogContent.displayName = DialogPrimitive.Content.displayName 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ) 66 | DialogHeader.displayName = "DialogHeader" 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ) 80 | DialogFooter.displayName = "DialogFooter" 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )) 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )) 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogTrigger, 114 | DialogClose, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | } 121 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | CaretSortIcon, 4 | CheckIcon, 5 | ChevronDownIcon, 6 | ChevronUpIcon, 7 | } from "@radix-ui/react-icons"; 8 | import * as SelectPrimitive from "@radix-ui/react-select"; 9 | 10 | import { cn } from "@/lib/utils"; 11 | 12 | const Select = SelectPrimitive.Root; 13 | 14 | const SelectGroup = SelectPrimitive.Group; 15 | 16 | const SelectValue = SelectPrimitive.Value; 17 | 18 | const SelectTrigger = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, children, ...props }, ref) => ( 22 | span]:line-clamp-1", 26 | className 27 | )} 28 | {...props} 29 | > 30 | {children} 31 | 32 | 33 | 34 | 35 | )); 36 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 37 | 38 | const SelectScrollUpButton = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | 51 | 52 | )); 53 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 54 | 55 | const SelectScrollDownButton = React.forwardRef< 56 | React.ElementRef, 57 | React.ComponentPropsWithoutRef 58 | >(({ className, ...props }, ref) => ( 59 | 67 | 68 | 69 | )); 70 | SelectScrollDownButton.displayName = 71 | SelectPrimitive.ScrollDownButton.displayName; 72 | 73 | const SelectContent = React.forwardRef< 74 | React.ElementRef, 75 | React.ComponentPropsWithoutRef 76 | >(({ className, children, position = "popper", ...props }, ref) => ( 77 | 78 | 89 | 90 | 97 | {children} 98 | 99 | 100 | 101 | 102 | )); 103 | SelectContent.displayName = SelectPrimitive.Content.displayName; 104 | 105 | const SelectLabel = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )); 115 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 116 | 117 | const SelectItem = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, children, ...props }, ref) => ( 121 | 129 | 130 | 131 | 132 | 133 | 134 | {children} 135 | 136 | )); 137 | SelectItem.displayName = SelectPrimitive.Item.displayName; 138 | 139 | const SelectSeparator = React.forwardRef< 140 | React.ElementRef, 141 | React.ComponentPropsWithoutRef 142 | >(({ className, ...props }, ref) => ( 143 | 148 | )); 149 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 150 | 151 | export { 152 | Select, 153 | SelectGroup, 154 | SelectValue, 155 | SelectTrigger, 156 | SelectContent, 157 | SelectLabel, 158 | SelectItem, 159 | SelectSeparator, 160 | SelectScrollUpButton, 161 | SelectScrollDownButton, 162 | }; 163 | -------------------------------------------------------------------------------- /src/core/actions.ts: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | import { core } from "./core"; 3 | import { ollamaRequest } from "./utils"; 4 | import { IModelType } from "./types"; 5 | 6 | interface sendProptOptions { 7 | prompt: string; 8 | model: string; 9 | context?: number[]; 10 | } 11 | 12 | export const sendPrompt = async (p: sendProptOptions) => { 13 | try { 14 | const res = await Axios({ 15 | method: "POST", 16 | url: `${core.server_host._value}/api/generate`, 17 | data: { 18 | model: p.model, 19 | prompt: p.prompt, 20 | stream: false, 21 | context: p?.context, 22 | }, 23 | }); 24 | 25 | return res.data; 26 | } catch (error) { 27 | throw error; 28 | } 29 | }; 30 | 31 | export const syncModels = async () => { 32 | const res = await ollamaRequest<{ models: IModelType[] }>("GET", "api/tags"); 33 | 34 | core.available_models.set( 35 | res.models.map((item) => ({ 36 | name: item.name, 37 | digest: item.digest, 38 | modified_at: item.modified_at, 39 | size: item.size, 40 | })) 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/core/core.ts: -------------------------------------------------------------------------------- 1 | import { SimpleCore } from "simple-core-state"; 2 | import { ICoreType } from "./types"; 3 | 4 | const instance = new SimpleCore( 5 | { 6 | database: { 7 | ready: false, 8 | }, 9 | conversations: [], 10 | focused_conv_data: [], 11 | focused_conv_id: "", 12 | focused_conv_meta: {} as any, 13 | server_host: "http://127.0.0.1:11435", 14 | server_connected: false, 15 | last_used_model: "", 16 | available_models: [], 17 | introduction_finished: false, 18 | }, 19 | { storage: { prefix: "ollama_web_ui_" } } 20 | ); 21 | 22 | instance.persist(["introduction_finished", "server_host", "last_used_model"]); 23 | 24 | instance.events.create(["trigger_settings"]); 25 | 26 | export const core = instance.core(); 27 | -------------------------------------------------------------------------------- /src/core/database-actions.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./local-database"; 2 | 3 | interface createConversationProps { 4 | id: string; 5 | title: string; 6 | created_at: Date; 7 | model: string; 8 | } 9 | 10 | export const createConversation = async (p: createConversationProps) => { 11 | console.log(Object.entries(p).map((item) => item[1])); 12 | 13 | try { 14 | await db.execute( 15 | "INSERT INTO conversations (id, title, created_at, model) VALUES ($1, $2, $3, $4)", 16 | [p.id, p.title, p.created_at, p.model] 17 | ); 18 | return true; 19 | } catch (error) { 20 | console.log(error); 21 | } 22 | }; 23 | 24 | export const getConversations = async () => { 25 | return await db.select("SELECT * FROM conversations"); 26 | }; 27 | 28 | export const getConversationMessages = async (id: string) => { 29 | return await db.select( 30 | "SELECT id, conversation_id, message, created_at, ai_replied, ctx FROM conversation_messages WHERE conversation_id = $1;", 31 | [id] 32 | ); 33 | }; 34 | 35 | interface SendPrompProps { 36 | id: string; 37 | conversation_id: string; 38 | message: string; 39 | created_at: Date; 40 | ai_replied: boolean; 41 | ctx: string; 42 | } 43 | 44 | export const sendPrompt = async (p: SendPrompProps) => { 45 | await db.execute( 46 | "INSERT INTO conversation_messages (id, conversation_id, message, created_at, ai_replied, ctx) VALUES ($1, $2, $3, $4, $5, $6)", 47 | [ 48 | p.id, 49 | p.conversation_id, 50 | p.message, 51 | p.created_at, 52 | p?.ai_replied ? 1 : 0, 53 | p?.ctx || null, 54 | ] 55 | ); 56 | return true; 57 | }; 58 | 59 | export const updateConversationName = async (name: string, conv_id: string) => { 60 | await db.execute("UPDATE conversations SET title = $1 WHERE id = $2", [ 61 | name, 62 | conv_id, 63 | ]); 64 | }; 65 | 66 | export const deleteConversation = async (id: string) => { 67 | await db.execute("DELETE FROM conversations WHERE id = $1;", [id]); 68 | await db.execute( 69 | "DELETE FROM conversation_messages WHERE conversation_id = $1;", 70 | [id] 71 | ); 72 | return true; 73 | }; 74 | 75 | export const prepareDatabase = async () => { 76 | // Create the conversations table 77 | await db.execute( 78 | `CREATE TABLE conversations ( 79 | id TEXT PRIMARY KEY, 80 | title TEXT NOT NULL, 81 | mode TEXT NOT NULL, 82 | created_at DATETIME NOT NULL 83 | );` 84 | ); 85 | 86 | // Create the conversation_messages table 87 | await db.execute(` 88 | CREATE TABLE conversation_messages ( 89 | id TEXT PRIMARY KEY, 90 | conversation_id TEXT NOT NULL, 91 | message TEXT NOT NULL, 92 | created_at DATETIME NOT NULL, 93 | ai_replied INTEGER NOT NULL 94 | FOREIGN KEY(conversation_id) REFERENCES conversations(id) 95 | ) 96 | `); 97 | 98 | return true; 99 | }; 100 | 101 | export const flushDatbase = async () => { 102 | await db.execute(`DELETE FROM conversation_messages;`); 103 | await db.execute(`DELETE FROM conversations;`); 104 | }; 105 | -------------------------------------------------------------------------------- /src/core/helper.ts: -------------------------------------------------------------------------------- 1 | import { trimWhitespace } from "."; 2 | 3 | export function generateRandomString(length: number): string { 4 | let randomString = ""; 5 | for (let i = 0; i < length; i++) { 6 | const num = Math.floor(Math.random() * 10); 7 | randomString += num.toString(); 8 | } 9 | return randomString; 10 | } 11 | 12 | export function extractTextAndCodeBlocks( 13 | inputString: string 14 | ): { content: string; type: "text" | "code" }[] { 15 | const codeBlockRegex = /```([\s\S]*?)```/g; 16 | const matches = []; 17 | let currentIndex = 0; 18 | 19 | inputString.replace(codeBlockRegex, (match, codeBlock, index) => { 20 | // Add the text before the code block to the array 21 | if (index > currentIndex) { 22 | const textBeforeCodeBlock = inputString 23 | .substring(currentIndex, index) 24 | .trim(); 25 | if (textBeforeCodeBlock.length > 0) { 26 | matches.push({ content: textBeforeCodeBlock, type: "text" }); 27 | } 28 | } 29 | 30 | // Add the code block to the array 31 | matches.push({ 32 | content: trimWhitespace(codeBlock), 33 | type: "code", 34 | who: "ollama", 35 | }); 36 | 37 | // Update the current index 38 | currentIndex = index + match.length; 39 | return match; 40 | }); 41 | 42 | // Add any remaining text after the last code block 43 | if (currentIndex < inputString.length) { 44 | const textAfterLastCodeBlock = inputString.substring(currentIndex).trim(); 45 | if (textAfterLastCodeBlock.length > 0) { 46 | matches.push({ content: textAfterLastCodeBlock, type: "text" }); 47 | } 48 | } 49 | 50 | return matches as any; 51 | } 52 | 53 | export function generateRandomId(length: number): string { 54 | const characters = 55 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 56 | let result = ""; 57 | const charactersLength = characters.length; 58 | for (let i = 0; i < length; i++) { 59 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 60 | } 61 | return result; 62 | } 63 | 64 | export const generateIdNumber = (input_length: number) => { 65 | let result = ""; 66 | let chars = "0123456789"; 67 | for (let i = 0; i < input_length; i++) { 68 | result += chars.charAt(Math.floor(Math.random() * chars.length)); 69 | } 70 | return result; 71 | }; 72 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils"; 2 | export * from "./core"; 3 | export * from "./helper"; 4 | export * as actions from "./database-actions"; 5 | export * from "./actions"; 6 | export const OLLAMA_HOST = `127.0.0.1:11435`; 7 | export const OLLAMA_COMMAND = `OLLAMA_ORIGINS=* OLLAMA_HOST=${OLLAMA_HOST} ollama serve`; 8 | -------------------------------------------------------------------------------- /src/core/local-database.ts: -------------------------------------------------------------------------------- 1 | import Database from "@tauri-apps/plugin-sql"; 2 | import { core } from "./core"; 3 | 4 | export let db: Database; 5 | 6 | export const loadDB = async () => { 7 | try { 8 | db = await Database.load("sqlite:ollama-chat.db"); 9 | core.database.patchObject({ ready: true }); 10 | } catch (error) { 11 | console.log("Something went wrong with loading the database", error); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | export interface ConversationMeta { 2 | id: string; 3 | created_at: Date; 4 | model: string; 5 | title: string; 6 | is_new?: boolean; 7 | } 8 | 9 | export type IConversations = ConversationMeta[]; 10 | export type ConversationMessages = ConversationMessage[]; 11 | 12 | export type ConversationMessage = { 13 | id: string; 14 | conversation_id: string; 15 | created_at: string; 16 | ai_replied: boolean; 17 | message: string; 18 | ctx?: string; 19 | }; 20 | 21 | export type IModelType = { 22 | name: string; 23 | digest: string; 24 | modified_at: string; 25 | size: number; 26 | }; 27 | 28 | export type ICoreType = { 29 | database: { 30 | ready: boolean; 31 | }; 32 | conversations: IConversations; 33 | focused_conv_id: string; 34 | focused_conv_data: ConversationMessages; 35 | focused_conv_meta: ConversationMeta; 36 | last_used_model: string; 37 | 38 | server_host: string; 39 | server_connected: boolean; 40 | available_models: IModelType[]; 41 | introduction_finished: boolean; 42 | }; 43 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | import { core } from "."; 3 | 4 | export const ollamaRequest = async ( 5 | m: "GET" | "POST", 6 | path: string, 7 | c?: { data?: any } 8 | ): Promise => { 9 | try { 10 | const res = await Axios({ 11 | method: m, 12 | url: `${core.server_host._value}/${path}`, 13 | data: c?.data, 14 | headers: { 15 | "Content-Type": "application/json", 16 | }, 17 | }); 18 | 19 | return res.data as T; 20 | } catch (error) { 21 | core.server_connected.set(false); 22 | throw error; 23 | } 24 | }; 25 | 26 | export const allomaGenerate = async ( 27 | prompt: string, 28 | mdl: string, 29 | ctx?: number[] 30 | ) => { 31 | try { 32 | const res = await ollamaRequest("POST", "api/generate", { 33 | data: { 34 | model: mdl, 35 | prompt: prompt, 36 | context: ctx, 37 | }, 38 | }); 39 | 40 | return res; 41 | } catch (error) { 42 | throw error; 43 | } 44 | }; 45 | 46 | export interface OllamaReturnObj { 47 | model: string; 48 | created_at: string; 49 | response: string; 50 | context?: number[]; 51 | done: boolean; 52 | total_duration?: number; 53 | load_duration?: number; 54 | prompt_eval_count?: number; 55 | prompt_eval_duration?: number; 56 | eval_count?: number; 57 | eval_duration?: number; 58 | } 59 | 60 | export function convertTextToJson(inputText: string): OllamaReturnObj[] { 61 | const lines = inputText.trim().split("\n"); 62 | const jsonArray = []; 63 | 64 | for (const line of lines) { 65 | const jsonObject = JSON.parse(line); 66 | jsonArray.push(jsonObject); 67 | } 68 | 69 | return jsonArray; 70 | } 71 | 72 | export const formatBytes = (bytes: number, decimals = 2) => { 73 | if (!+bytes) return "0 Bytes"; 74 | 75 | const k = 1024; 76 | const dm = decimals < 0 ? 0 : decimals; 77 | const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 78 | 79 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 80 | 81 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; 82 | }; 83 | export function trimWhitespace(str: string): string { 84 | return str.replace(/^[\s\xA0]+|[\s\xA0]+$/g, ""); 85 | } 86 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 0 0% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 3.9%; 13 | --primary: 0 0% 9%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 0 0% 96.1%; 16 | --secondary-foreground: 0 0% 9%; 17 | --muted: 0 0% 96.1%; 18 | --muted-foreground: 0 0% 45.1%; 19 | --accent: 0 0% 96.1%; 20 | --accent-foreground: 0 0% 9%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 0 0% 89.8%; 24 | --input: 0 0% 89.8%; 25 | --ring: 0 0% 3.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 0 0% 3.9%; 36 | --foreground: 0 0% 98%; 37 | --card: 0 0% 3.9%; 38 | --card-foreground: 0 0% 98%; 39 | --popover: 0 0% 3.9%; 40 | --popover-foreground: 0 0% 98%; 41 | --primary: 0 0% 98%; 42 | --primary-foreground: 0 0% 9%; 43 | --secondary: 0 0% 14.9%; 44 | --secondary-foreground: 0 0% 98%; 45 | --muted: 0 0% 14.9%; 46 | --muted-foreground: 0 0% 63.9%; 47 | --accent: 0 0% 14.9%; 48 | --accent-foreground: 0 0% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 0 0% 98%; 51 | --border: 0 0% 14.9%; 52 | --input: 0 0% 14.9%; 53 | --ring: 0 0% 83.1%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | 71 | body, 72 | html { 73 | height: 100%; 74 | } 75 | 76 | #root { 77 | height: 100%; 78 | } 79 | 80 | /* Hide scrollbar for Chrome, Safari and Opera */ 81 | .no-scrollbar::-webkit-scrollbar { 82 | display: none; 83 | } 84 | 85 | /* Hide scrollbar for IE, Edge and Firefox */ 86 | .no-scrollbar { 87 | -ms-overflow-style: none; /* IE and Edge */ 88 | scrollbar-width: none; /* Firefox */ 89 | } 90 | 91 | .drag-window { 92 | -webkit-user-select: none; 93 | -webkit-app-region: drag; 94 | } 95 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { AppFrame } from "./app/index"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | }, 70 | animation: { 71 | "accordion-down": "accordion-down 0.2s ease-out", 72 | "accordion-up": "accordion-up 0.2s ease-out", 73 | }, 74 | }, 75 | }, 76 | plugins: [require("tailwindcss-animate")], 77 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src", "tsconfig.json"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { resolve } from "node:path"; 4 | 5 | // @ts-expect-error process is a nodejs global 6 | const host = process.env.TAURI_DEV_HOST; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(async () => ({ 10 | plugins: [react()], 11 | resolve: { 12 | alias: { 13 | "@": resolve(__dirname, "./src"), 14 | }, 15 | }, 16 | 17 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 18 | // 19 | // 1. prevent vite from obscuring rust errors 20 | clearScreen: false, 21 | // 2. tauri expects a fixed port, fail if that port is not available 22 | server: { 23 | port: 1420, 24 | strictPort: true, 25 | host: host || false, 26 | hmr: host 27 | ? { 28 | protocol: "ws", 29 | host, 30 | port: 1421, 31 | } 32 | : undefined, 33 | watch: { 34 | // 3. tell vite to ignore watching `src-tauri` 35 | ignored: ["**/src-tauri/**"], 36 | }, 37 | }, 38 | })); 39 | --------------------------------------------------------------------------------