├── .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 |
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 |
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 |
30 | ) : (
31 | <>>
32 | )}
33 |
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 |
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 |
--------------------------------------------------------------------------------