├── src
├── vite-env.d.ts
├── components
│ ├── ErrorsToastText.ts
│ ├── SimplePagination.tsx
│ ├── ui
│ │ ├── label.tsx
│ │ ├── collapsible.tsx
│ │ ├── sonner.tsx
│ │ ├── textarea.tsx
│ │ ├── switch.tsx
│ │ ├── input.tsx
│ │ ├── toggle.tsx
│ │ ├── badge.tsx
│ │ ├── popover.tsx
│ │ ├── card.tsx
│ │ ├── alert.tsx
│ │ ├── tabs.tsx
│ │ ├── tooltip.tsx
│ │ ├── button.tsx
│ │ ├── resizable.tsx
│ │ ├── drawer.tsx
│ │ ├── form.tsx
│ │ ├── dialog.tsx
│ │ ├── alert-dialog.tsx
│ │ └── command.tsx
│ ├── RelativeTime.tsx
│ ├── SearchApisContext.tsx
│ ├── SettingsFeatureToggle.tsx
│ ├── ReasoningDisplay.tsx
│ ├── ScrollToBottom.tsx
│ ├── NavigationDescription.tsx
│ ├── CodeBlock.tsx
│ ├── ToolCallDisplay.tsx
│ ├── Markdown.tsx
│ ├── AIProvidersContext.tsx
│ ├── ChatHistory.tsx
│ └── ModelSelectorContext.tsx
├── routes
│ ├── index.tsx
│ ├── translate.tsx
│ ├── _settingsLayout.settings.index.tsx
│ ├── _settingsLayout.theme-configure.tsx
│ ├── _settingsLayout.settings.manage-search-apis.tsx
│ ├── _settingsLayout.settings.manage-default-models.tsx
│ ├── chat.$chatId.tsx
│ └── _settingsLayout.tsx
├── lib
│ ├── relative-time.ts
│ ├── chat-path-store.ts
│ ├── search-api-store.ts
│ ├── common-ai-tools.ts
│ ├── default-model-store.ts
│ ├── consts.ts
│ ├── utils.ts
│ ├── search-ai-tools.ts
│ ├── github-get-latest-release-response-schema.d.ts
│ ├── chat-store.ts
│ └── updater.ts
├── hooks
│ └── useIsDark.ts
├── main.tsx
├── assets
│ └── react.svg
├── features
│ ├── search-api-management.tsx
│ └── default-model-management.tsx
└── App.css
├── app-icon.png
├── src-tauri
├── build.rs
├── icons
│ ├── icon.ico
│ ├── icon.png
│ ├── 128x128.png
│ ├── 32x32.png
│ ├── 64x64.png
│ ├── icon.icns
│ ├── StoreLogo.png
│ ├── 128x128@2x.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ └── ios
│ │ ├── AppIcon-512@2x.png
│ │ ├── AppIcon-20x20@1x.png
│ │ ├── AppIcon-20x20@2x-1.png
│ │ ├── AppIcon-20x20@2x.png
│ │ ├── AppIcon-20x20@3x.png
│ │ ├── AppIcon-29x29@1x.png
│ │ ├── AppIcon-29x29@2x-1.png
│ │ ├── AppIcon-29x29@2x.png
│ │ ├── AppIcon-29x29@3x.png
│ │ ├── AppIcon-40x40@1x.png
│ │ ├── AppIcon-40x40@2x-1.png
│ │ ├── AppIcon-40x40@2x.png
│ │ ├── AppIcon-40x40@3x.png
│ │ ├── AppIcon-60x60@2x.png
│ │ ├── AppIcon-60x60@3x.png
│ │ ├── AppIcon-76x76@1x.png
│ │ ├── AppIcon-76x76@2x.png
│ │ └── AppIcon-83.5x83.5@2x.png
├── gen
│ └── android
│ │ ├── settings.gradle
│ │ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ │ ├── app
│ │ ├── src
│ │ │ └── main
│ │ │ │ ├── res
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ ├── ic_launcher_round.png
│ │ │ │ │ └── ic_launcher_foreground.png
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ ├── themes.xml
│ │ │ │ │ └── colors.xml
│ │ │ │ ├── xml
│ │ │ │ │ └── file_paths.xml
│ │ │ │ ├── values-night
│ │ │ │ │ └── themes.xml
│ │ │ │ ├── layout
│ │ │ │ │ └── activity_main.xml
│ │ │ │ ├── drawable-v24
│ │ │ │ │ └── ic_launcher_foreground.xml
│ │ │ │ └── drawable
│ │ │ │ │ └── ic_launcher_background.xml
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ └── java
│ │ │ │ └── xyz
│ │ │ │ └── lightm
│ │ │ │ └── taiga
│ │ │ │ └── app
│ │ │ │ └── MainActivity.kt
│ │ ├── .gitignore
│ │ ├── proguard-rules.pro
│ │ └── build.gradle.kts
│ │ ├── .editorconfig
│ │ ├── .gitignore
│ │ ├── buildSrc
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ └── java
│ │ │ └── xyz
│ │ │ └── lightm
│ │ │ └── taiga
│ │ │ └── app
│ │ │ └── kotlin
│ │ │ ├── BuildTask.kt
│ │ │ └── RustPlugin.kt
│ │ ├── build.gradle.kts
│ │ ├── gradle.properties
│ │ ├── gradlew.bat
│ │ └── gradlew
├── .gitignore
├── src
│ ├── main.rs
│ └── lib.rs
├── capabilities
│ └── default.json
├── Cargo.toml
└── tauri.conf.json
├── .eslintignore
├── public
├── 310x310Logo.png
├── screenshot-1.jpg
├── screenshot-2.jpg
├── fonts
│ ├── Aeche-bold.woff2
│ ├── Aeche-black.woff2
│ ├── Aeche-light.woff2
│ ├── Aeche-regular.woff2
│ └── Aeche-extrabold.woff2
├── vite.svg
├── preset-themes
│ └── taiga.css
└── tauri.svg
├── .vscode
└── extensions.json
├── tsconfig.node.json
├── .swcrc
├── .gitignore
├── index.html
├── components.json
├── lingui.config.js
├── .eslintrc
├── tsconfig.json
├── prettier.config.cjs
├── vite.config.ts
├── package.json
└── README.md
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/app-icon.png
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/*
2 | .cache
3 | public
4 | node_modules
5 | *.esm.js
6 |
--------------------------------------------------------------------------------
/public/310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/310x310Logo.png
--------------------------------------------------------------------------------
/public/screenshot-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/screenshot-1.jpg
--------------------------------------------------------------------------------
/public/screenshot-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/screenshot-2.jpg
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | apply from: 'tauri.settings.gradle'
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/64x64.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/public/fonts/Aeche-bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/fonts/Aeche-bold.woff2
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/public/fonts/Aeche-black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/fonts/Aeche-black.woff2
--------------------------------------------------------------------------------
/public/fonts/Aeche-light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/fonts/Aeche-light.woff2
--------------------------------------------------------------------------------
/public/fonts/Aeche-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/fonts/Aeche-regular.woff2
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
3 | }
4 |
--------------------------------------------------------------------------------
/public/fonts/Aeche-extrabold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/public/fonts/Aeche-extrabold.woff2
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Taiga
3 | Taiga
4 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src/components/ErrorsToastText.ts:
--------------------------------------------------------------------------------
1 | import { t } from "@lingui/core/macro"
2 |
3 | export const ErrorsToastText = {
4 | TimeoutError: t`Request timeout`,
5 | unknown: t`Unknown error`,
6 | }
7 |
--------------------------------------------------------------------------------
/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import Chat from '@/features/chat'
2 | import { createFileRoute } from '@tanstack/react-router'
3 |
4 | export const Route = createFileRoute('/')({
5 | component: Chat,
6 | })
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ayuilos/Taiga/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/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/gen/android/app/.gitignore:
--------------------------------------------------------------------------------
1 | /src/main/java/xyz/lightm/taiga/app/generated
2 | /src/main/jniLibs/**/*.so
3 | /src/main/assets/tauri.conf.json
4 | /tauri.build.gradle.kts
5 | /proguard-tauri.pro
6 | /tauri.properties
--------------------------------------------------------------------------------
/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 | taiga_lib::run()
6 | }
7 |
--------------------------------------------------------------------------------
/src/routes/translate.tsx:
--------------------------------------------------------------------------------
1 | import Translator from "@/features/translator"
2 | import { createFileRoute } from "@tanstack/react-router"
3 |
4 | export const Route = createFileRoute("/translate")({
5 | component: Translator,
6 | })
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/relative-time.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs"
2 | import relativeTime from "dayjs/plugin/relativeTime"
3 |
4 | dayjs.extend(relativeTime)
5 |
6 | export function getRelativeTime(date: Date) {
7 | const targetDate = dayjs(date)
8 |
9 | return targetDate.fromNow()
10 | }
11 |
--------------------------------------------------------------------------------
/src/routes/_settingsLayout.settings.index.tsx:
--------------------------------------------------------------------------------
1 | import ProviderManagement from "@/features/provider-management"
2 | import { createFileRoute } from "@tanstack/react-router"
3 |
4 | export const Route = createFileRoute("/_settingsLayout/settings/")({
5 | component: ProviderManagement,
6 | })
7 |
--------------------------------------------------------------------------------
/src/routes/_settingsLayout.theme-configure.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeConfigure } from "@/features/theme-configure"
2 | import { createFileRoute } from "@tanstack/react-router"
3 |
4 | export const Route = createFileRoute("/_settingsLayout/theme-configure")({
5 | component: ThemeConfigure,
6 | })
7 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue May 10 19:22:52 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = false
12 | insert_final_newline = false
--------------------------------------------------------------------------------
/src/routes/_settingsLayout.settings.manage-search-apis.tsx:
--------------------------------------------------------------------------------
1 | import { SearchAPIManagement } from "@/features/search-api-management"
2 | import { createFileRoute } from "@tanstack/react-router"
3 |
4 | export const Route = createFileRoute("/_settingsLayout/settings/manage-search-apis")({
5 | component: SearchAPIManagement,
6 | })
7 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/swcrc",
3 | "jsc": {
4 | "experimental": {
5 | "plugins": [
6 | [
7 | "@lingui/swc-plugin",
8 | {
9 | // Additional Configuration
10 | }
11 | ]
12 | ]
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/routes/_settingsLayout.settings.manage-default-models.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultModelManagement } from "@/features/default-model-management"
2 | import { createFileRoute } from "@tanstack/react-router"
3 |
4 | export const Route = createFileRoute("/_settingsLayout/settings/manage-default-models")(
5 | {
6 | component: DefaultModelManagement,
7 | }
8 | )
9 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | key.properties
17 |
18 | /.tauri
19 | /tauri.settings.gradle
--------------------------------------------------------------------------------
/.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 | keystore.properties
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Tauri + React + Typescript
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | gradlePlugin {
6 | plugins {
7 | create("pluginsForCoolKids") {
8 | id = "rust"
9 | implementationClass = "RustPlugin"
10 | }
11 | }
12 | }
13 |
14 | repositories {
15 | google()
16 | mavenCentral()
17 | }
18 |
19 | dependencies {
20 | compileOnly(gradleApi())
21 | implementation("com.android.tools.build:gradle:8.5.1")
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | dependencies {
7 | classpath("com.android.tools.build:gradle:8.5.1")
8 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
9 | }
10 | }
11 |
12 | allprojects {
13 | repositories {
14 | google()
15 | mavenCentral()
16 | }
17 | }
18 |
19 | tasks.register("clean").configure {
20 | delete("build")
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/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": "",
8 | "css": "src/App.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/src/components/SimplePagination.tsx:
--------------------------------------------------------------------------------
1 | export function SimplePagination({
2 | page,
3 | setPage,
4 | totalPages,
5 | }: {
6 | page: number
7 | setPage: (page: number) => void
8 | totalPages: number
9 | }) {
10 | return (
11 |
12 | {page > 1 && setPage(page - 1)}>{"<"} }
13 |
14 | {page} / {totalPages}
15 |
16 | {page < totalPages && (
17 | setPage(page + 1)}>{">"}
18 | )}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/routes/chat.$chatId.tsx:
--------------------------------------------------------------------------------
1 | import Chat from '@/features/chat'
2 | import { LastChatIDStore, TChatID } from '@/lib/chat-store'
3 | import { createFileRoute } from '@tanstack/react-router'
4 | import { useEffect } from 'react'
5 |
6 | export const Route = createFileRoute('/chat/$chatId')({
7 | component: RouteComponent,
8 | })
9 |
10 | function RouteComponent() {
11 | const { chatId } = Route.useParams()
12 |
13 | useEffect(() => {
14 | LastChatIDStore.setLastChatID(chatId as TChatID)
15 | }, [chatId])
16 |
17 | return
18 | }
19 |
--------------------------------------------------------------------------------
/lingui.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@lingui/cli"
2 |
3 | export default defineConfig({
4 | sourceLocale: "en",
5 | locales: [
6 | "ar-AE",
7 | "bn-BD",
8 | "de-DE",
9 | "en",
10 | "en-US",
11 | "es-ES",
12 | "fr-FR",
13 | "hi-IN",
14 | "id-ID",
15 | "ja-JP",
16 | "ko-KR",
17 | "pt-BR",
18 | "ru-RU",
19 | "th-TH",
20 | "ur-PK",
21 | "vi-VN",
22 | "zh-CN",
23 | "zh-HK",
24 | "zh-TW",
25 | ],
26 | catalogs: [
27 | {
28 | path: "/src/locales/{locale}/messages",
29 | include: ["src"],
30 | },
31 | ],
32 | })
33 |
--------------------------------------------------------------------------------
/src/lib/chat-path-store.ts:
--------------------------------------------------------------------------------
1 | import { load } from "@tauri-apps/plugin-store"
2 |
3 | const chatPathStoreKey = "chat-path-store.json"
4 |
5 | export class ChatPathStore {
6 | static async getChatPath(id: string) {
7 | const store = await load(chatPathStoreKey)
8 | const chatPath = await store.get(id)
9 | if (!chatPath) {
10 | throw new Error(`Chat path for chat with id ${id} not found`)
11 | }
12 | return chatPath as number[]
13 | }
14 |
15 | static async setChatPath(id: string, chatPath: number[]) {
16 | const store = await load(chatPathStoreKey)
17 | await store.set(id, chatPath)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | // Waiting tailwindcss plugin to support tailwind v4
5 | "extends": ["plugin:react-hooks/recommended"],
6 | "rules": {
7 | "react/jsx-key": "off",
8 | "react/no-unescaped-entities": "off",
9 | "tailwindcss/no-custom-classname": "off",
10 | },
11 | "settings": {
12 | "tailwindcss": {
13 | "callees": ["cn"],
14 | "config": "tailwind.config.js",
15 | },
16 | },
17 | "overrides": [
18 | {
19 | "files": ["*.ts", "*.tsx", "*.jsx"],
20 | "parser": "@typescript-eslint/parser",
21 | },
22 | ],
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/useIsDark.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 |
3 | export function useIsDark() {
4 | const [isDark, setIsDark] = useState(
5 | window.matchMedia("(prefers-color-scheme: dark)").matches
6 | )
7 |
8 | useEffect(() => {
9 | const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
10 |
11 | const handleSystemThemeChange = (e: MediaQueryListEvent) => {
12 | setIsDark(e.matches)
13 | }
14 |
15 | mediaQuery.addEventListener("change", handleSystemThemeChange)
16 |
17 | return () => {
18 | mediaQuery.removeEventListener("change", handleSystemThemeChange)
19 | }
20 | }, [])
21 |
22 | return isDark
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Label({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
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_http::init())
12 | .plugin(tauri_plugin_store::Builder::new().build())
13 | .plugin(tauri_plugin_opener::init())
14 | .invoke_handler(tauri::generate_handler![greet])
15 | .run(tauri::generate_context!())
16 | .expect("error while running tauri application");
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "allowJs": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 | "baseUrl": ".",
10 | "paths": {
11 | "@/*": ["./src/*"]
12 | },
13 |
14 | /* Bundler mode */
15 | "moduleResolution": "bundler",
16 | "allowImportingTsExtensions": true,
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 |
22 | /* Linting */
23 | "strict": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": true,
26 | "noFallthroughCasesInSwitch": true
27 | },
28 | "include": ["src"],
29 | "references": [{ "path": "./tsconfig.node.json" }]
30 | }
31 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | importOrder: [
9 | "^(react/(.*)$)|^(react$)",
10 | "^(next/(.*)$)|^(next$)",
11 | "",
12 | "",
13 | "^types$",
14 | "^@/types/(.*)$",
15 | "^@/config/(.*)$",
16 | "^@/lib/(.*)$",
17 | "^@/components/(.*)$",
18 | "^@/styles/(.*)$",
19 | "^[./]",
20 | ],
21 | importOrderSeparation: false,
22 | importOrderSortSpecifiers: true,
23 | importOrderBuiltinModulesToTop: true,
24 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
25 | importOrderMergeDuplicateImports: true,
26 | importOrderCombineTypeAndValueImports: true,
27 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
28 | };
29 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/search-api-store.ts:
--------------------------------------------------------------------------------
1 | import { load } from "@tauri-apps/plugin-store"
2 | import { z } from "zod"
3 |
4 | const searchAPIStoreKey = "search-api-store.json"
5 |
6 | export const SearchApiSchema = z.object({
7 | name: z.string().min(1),
8 | apiKey: z.string().optional(),
9 | searchURL: z.string().url(),
10 | })
11 |
12 | export type TBaseSearchAPI = z.infer
13 |
14 | export class SearchApiStore {
15 | static async getSearchApi(name: string) {
16 | const store = await load(searchAPIStoreKey)
17 | return store.get(name)
18 | }
19 |
20 | static async getAllSearchApis() {
21 | const store = await load(searchAPIStoreKey)
22 |
23 | return store.values()
24 | }
25 |
26 | static async setSearchApi(api: TBaseSearchAPI) {
27 | const store = await load(searchAPIStoreKey)
28 | await store.set(api.name, api)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | function Collapsible({
4 | ...props
5 | }: React.ComponentProps) {
6 | return
7 | }
8 |
9 | function CollapsibleTrigger({
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
17 | )
18 | }
19 |
20 | function CollapsibleContent({
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
28 | )
29 | }
30 |
31 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
32 |
--------------------------------------------------------------------------------
/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 | "core:default",
10 | "opener:default",
11 | "store:default",
12 | {
13 | "identifier": "http:default",
14 | "allow": [
15 | {
16 | "url": "https://*"
17 | },
18 | {
19 | "url": "http://localhost:*"
20 | },
21 | {
22 | "url": "http://127.0.0.1:*"
23 | },
24 | {
25 | "url": "http://*.localhost:*"
26 | },
27 | {
28 | "url": "http://(192\\.168).*:*"
29 | }
30 | ]
31 | },
32 | "opener:default",
33 | "fs:default",
34 | "fs:allow-exists",
35 | "fs:allow-app-read-recursive",
36 | "fs:allow-app-write-recursive",
37 | "fs:allow-watch"
38 | ]
39 | }
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "Taiga"
3 | version = "0.1.0-rc.10"
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 | # The `_lib` suffix may seem redundant but it is necessary
12 | # to make the lib name unique and wouldn't conflict with the bin name.
13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
14 | name = "taiga_lib"
15 | crate-type = ["staticlib", "cdylib", "rlib"]
16 |
17 | [build-dependencies]
18 | tauri-build = { version = "2", features = [] }
19 |
20 | [dependencies]
21 | tauri = { version = "2", features = ["protocol-asset"] }
22 | tauri-plugin-opener = "2"
23 | serde = { version = "1", features = ["derive"] }
24 | serde_json = "1"
25 | tauri-plugin-store = "2"
26 | tauri-plugin-http = "2"
27 | tauri-plugin-fs = { version = "2.2.1", features = ["watch"] }
28 |
29 |
--------------------------------------------------------------------------------
/src/lib/common-ai-tools.ts:
--------------------------------------------------------------------------------
1 | import { calculator } from "@agentic/calculator"
2 | import { AIFunctionSet, createAIFunction } from "@agentic/core"
3 | import { z } from "zod"
4 |
5 | // TODO: ensure `expr` is sanitized to not run arbitrary code
6 | export const CurrentTimeInputSchema = z.object({
7 | iso: z.boolean().optional(),
8 | utc: z.boolean().optional(),
9 | })
10 | export type CalculatorInput = z.infer
11 |
12 | const currentTime = createAIFunction(
13 | {
14 | name: "CurrentTime",
15 | description:
16 | "Get current time in ISO 8601 Extended Format(YYYY-MM-DDTHH:mm:ss.sssZ) or UTC string format. Will use ISO by default.",
17 | inputSchema: CurrentTimeInputSchema,
18 | },
19 | async ({ iso, utc }: CalculatorInput) => {
20 | return new Date()[
21 | iso ? "toISOString" : utc ? "toUTCString" : "toISOString"
22 | ]()
23 | }
24 | )
25 |
26 | export const commonAITools = new AIFunctionSet([calculator, currentTime])
27 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes"
2 | import { Toaster as Sonner, ToasterProps } from "sonner"
3 |
4 | const Toaster = ({ ...props }: ToasterProps) => {
5 | const { theme = "system" } = useTheme()
6 |
7 | return (
8 |
26 | )
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/src/lib/default-model-store.ts:
--------------------------------------------------------------------------------
1 | import { load } from "@tauri-apps/plugin-store"
2 |
3 | const defaultModelStoreKey = "default-model.json"
4 |
5 | export type TDefaultModel = [string, string] | null
6 | export type TDefaultModels = Record<
7 | "translate" | "chat" | "summarize",
8 | TDefaultModel
9 | >
10 |
11 | export class DefaultModelStore {
12 | static async getDefaultModels() {
13 | const store = await load(defaultModelStoreKey)
14 | const models: TDefaultModels = {
15 | translate: null,
16 | chat: null,
17 | summarize: null,
18 | }
19 | const saveddefaultModels = (await store.entries()) as [
20 | keyof TDefaultModels,
21 | TDefaultModel,
22 | ][]
23 |
24 | for (const [key, value] of saveddefaultModels) {
25 | models[key] = value
26 | }
27 |
28 | return models
29 | }
30 |
31 | static async setDefaultModel(type: keyof TDefaultModels, model: TDefaultModel) {
32 | const store = await load(defaultModelStoreKey)
33 | await store.set(type, model)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.tauri.app/config/2",
3 | "productName": "Taiga",
4 | "version": "0.1.0-rc.10",
5 | "identifier": "xyz.lightm.taiga.app",
6 | "build": {
7 | "beforeDevCommand": "yarn dev",
8 | "devUrl": "http://localhost:1420",
9 | "beforeBuildCommand": "yarn build",
10 | "frontendDist": "../dist"
11 | },
12 | "app": {
13 | "windows": [
14 | {
15 | "title": "taiga",
16 | "width": 800,
17 | "height": 600
18 | }
19 | ],
20 | "security": {
21 | "csp": null,
22 | "assetProtocol": {
23 | "enable": true,
24 | "scope": {
25 | "requireLiteralLeadingDot": false,
26 | "allow": ["**/*"]
27 | }
28 | }
29 | }
30 | },
31 | "bundle": {
32 | "active": true,
33 | "targets": "all",
34 | "icon": [
35 | "icons/32x32.png",
36 | "icons/64x64.png",
37 | "icons/128x128.png",
38 | "icons/128x128@2x.png",
39 | "icons/icon.icns",
40 | "icons/icon.ico"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/routes/_settingsLayout.tsx:
--------------------------------------------------------------------------------
1 | import { SiGithub } from "@icons-pack/react-simple-icons"
2 | import { createFileRoute, Outlet } from "@tanstack/react-router"
3 | import { openUrl } from "@tauri-apps/plugin-opener"
4 |
5 | import { getCurrentVersion, TAIGA_GITHUB_URL } from "@/lib/updater"
6 |
7 | export const Route = createFileRoute("/_settingsLayout")({
8 | component: SettingsLayout,
9 | })
10 |
11 | function SettingsLayout() {
12 | const currentVersion = getCurrentVersion()
13 |
14 | return (
15 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/src/components/RelativeTime.tsx:
--------------------------------------------------------------------------------
1 | import { getRelativeTime } from "@/lib/relative-time"
2 | import { memo, useEffect, useState } from "react"
3 |
4 | interface IRelativeTime {
5 | defaultOn?: boolean
6 | autoRefresh?: boolean
7 | date: number | string
8 | }
9 | function UnMemoizedRelativeTime({
10 | date,
11 | defaultOn = true,
12 | autoRefresh = true,
13 | }: IRelativeTime) {
14 | const dateObj = new Date(date)
15 | const [isRelative, setIsRelative] = useState(defaultOn)
16 | // Use bit calculate to trigger re-render to bring the lowest cost
17 | // Too low bits is risky which may not trigger re-render under some situation
18 | const [bits, setBits] = useState(0b000)
19 |
20 | useEffect(() => {
21 | if (autoRefresh && isRelative) {
22 | setTimeout(() => setBits((b) => (b + 0b001) & 0b111), 1000)
23 | }
24 | }, [bits, autoRefresh, isRelative])
25 |
26 | return (
27 | setIsRelative((r) => !r)}>
28 | {isRelative ? getRelativeTime(dateObj) : dateObj.toLocaleString()}
29 |
30 | )
31 | }
32 | export const RelativeTime = memo(UnMemoizedRelativeTime)
33 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitive from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Switch({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 |
25 |
26 | )
27 | }
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/lib/consts.ts:
--------------------------------------------------------------------------------
1 | export enum LANG_CODE {
2 | AUTO = "AUTO",
3 | LANG_EN = "LANG_EN",
4 | LANG_ZH = "LANG_ZH",
5 | LANG_ZH_TW = "LANG_ZH_TW",
6 | LANG_HI = "LANG_HI",
7 | LANG_ES = "LANG_ES",
8 | LANG_AR = "LANG_AR",
9 | LANG_FR = "LANG_FR",
10 | LANG_BN = "LANG_BN",
11 | LANG_PT = "LANG_PT",
12 | LANG_RU = "LANG_RU",
13 | LANG_UR = "LANG_UR",
14 | LANG_ID = "LANG_ID",
15 | LANG_DE = "LANG_DE",
16 | LANG_JA = "LANG_JA",
17 | LANG_VI = "LANG_VI",
18 | LANG_TH = "LANG_TH",
19 | LANG_KO = "LANG_KO",
20 | }
21 |
22 | export const LANG = {
23 | [LANG_CODE.AUTO]: "Auto",
24 | [LANG_CODE.LANG_EN]: "English",
25 | [LANG_CODE.LANG_ZH]: "Chinese(Simplified)",
26 | [LANG_CODE.LANG_ZH_TW]: "Chinese(Traditional)",
27 | [LANG_CODE.LANG_HI]: "Hindi",
28 | [LANG_CODE.LANG_ES]: "Spanish",
29 | [LANG_CODE.LANG_AR]: "Arabic",
30 | [LANG_CODE.LANG_FR]: "French",
31 | [LANG_CODE.LANG_BN]: "Bengali",
32 | [LANG_CODE.LANG_PT]: "Portuguese",
33 | [LANG_CODE.LANG_RU]: "Russian",
34 | [LANG_CODE.LANG_UR]: "Urdu",
35 | [LANG_CODE.LANG_ID]: "Indonesian",
36 | [LANG_CODE.LANG_DE]: "German",
37 | [LANG_CODE.LANG_JA]: "Japanese",
38 | [LANG_CODE.LANG_VI]: "Vietnamese",
39 | [LANG_CODE.LANG_TH]: "Thai",
40 | [LANG_CODE.LANG_KO]: "Korean",
41 | } as const
42 |
--------------------------------------------------------------------------------
/src/components/SearchApisContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, PropsWithChildren, useCallback, useState } from "react"
2 |
3 | import { SearchApiStore, TBaseSearchAPI } from "@/lib/search-api-store"
4 |
5 | type TSearchApisContextType = ReturnType
6 | export const SearchApisContext = createContext({
7 | searchApis: [],
8 | fetchSearchApis: async () => [],
9 | setSearchApi: async () => {},
10 | })
11 |
12 | export function SearchApisContextProvider({ children }: PropsWithChildren) {
13 | const context = useSearchApisContext()
14 |
15 | return (
16 |
17 | {children}
18 |
19 | )
20 | }
21 |
22 | function useSearchApisContext() {
23 | const [searchApis, setSearchApis] = useState([])
24 |
25 | const fetchSearchApis = useCallback(async () => {
26 | const _searchAPIs = await SearchApiStore.getAllSearchApis()
27 |
28 | setSearchApis(_searchAPIs)
29 |
30 | return _searchAPIs
31 | }, [])
32 |
33 | const setSearchApi = useCallback(
34 | async (api: TBaseSearchAPI) => {
35 | await SearchApiStore.setSearchApi(api)
36 | await fetchSearchApis()
37 | },
38 | [fetchSearchApis]
39 | )
40 |
41 | return {
42 | searchApis,
43 | fetchSearchApis,
44 | setSearchApi,
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
16 | )
17 | }
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/src/components/SettingsFeatureToggle.tsx:
--------------------------------------------------------------------------------
1 | import { t } from "@lingui/core/macro"
2 | import { useLocation, useRouter } from "@tanstack/react-router"
3 |
4 | import {
5 | Select,
6 | SelectContent,
7 | SelectGroup,
8 | SelectItem,
9 | SelectTrigger,
10 | SelectValue,
11 | } from "@/components/ui/select"
12 |
13 | export function SettingsFeatureToggle() {
14 | const location = useLocation()
15 | const router = useRouter()
16 |
17 | const providerItemString = t`Providers`
18 | const modelItemString = t`Default Models`
19 | const searchApiItemString = t`Search APIs`
20 |
21 | return (
22 | {
25 | router.navigate({
26 | to: path,
27 | replace: true,
28 | })
29 | }}
30 | >
31 |
32 |
33 |
34 |
35 |
36 |
37 | {providerItemString}
38 |
39 |
40 | {modelItemString}
41 |
42 |
43 | {searchApiItemString}
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 | import { i18n } from "@lingui/core"
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export async function dynamicActivate(locale: string) {
10 | const { messages } = await import(`../locales/${locale}/messages.po`)
11 |
12 | i18n.load(locale, messages)
13 | i18n.activate(locale)
14 | }
15 |
16 | export function stringifyObject(ob: any) {
17 | return JSON.stringify(ob, null, 2)
18 | }
19 |
20 | export function diffStringArrays(
21 | oldArray: string[],
22 | newArray: string[]
23 | ): { added: string[]; removed: string[] } {
24 | const added = newArray.filter((item) => !oldArray.includes(item))
25 | const removed = oldArray.filter((item) => !newArray.includes(item))
26 |
27 | return { added, removed }
28 | }
29 |
30 | export function sameArrays(arr1: any[], arr2: any[]) {
31 | if (arr1.length !== arr2.length) {
32 | return false
33 | }
34 |
35 | for (let i = 0; i < arr1.length; i++) {
36 | if (arr1[i] !== arr2[i]) {
37 | return false
38 | }
39 | }
40 |
41 | return true
42 | }
43 |
44 | export function addCSSLinkTag(url: string, dataset?: Record) {
45 | const link = document.createElement("link")
46 | link.rel = "stylesheet"
47 | link.href = url
48 | if (dataset) {
49 | for (const [key, value] of Object.entries(dataset)) {
50 | link.dataset[key] = value
51 | }
52 | }
53 | document.head.appendChild(link)
54 | }
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { i18n } from "@lingui/core"
3 | import { I18nProvider } from "@lingui/react"
4 | import { createRouter, RouterProvider } from "@tanstack/react-router"
5 | import ReactDOM from "react-dom/client"
6 |
7 | import AIProviderContextProvider from "./components/AIProvidersContext"
8 | import { ModelSelectorContextProvider } from "./components/ModelSelectorContext"
9 | import { SearchApisContextProvider } from "./components/SearchApisContext"
10 | import { ThemeProvider } from "./components/ThemeProvider"
11 | import { TooltipProvider } from "./components/ui/tooltip"
12 | // Import the generated route tree
13 | import { routeTree } from "./routeTree.gen"
14 |
15 | // Create a new router instance
16 | const router = createRouter({ routeTree })
17 |
18 | // Register the router instance for type safety
19 | declare module "@tanstack/react-router" {
20 | interface Register {
21 | router: typeof router
22 | }
23 | }
24 |
25 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0 transition-[color,box-shadow]",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-9 px-2 min-w-9",
18 | sm: "h-8 px-1.5 min-w-8",
19 | lg: "h-10 px-2.5 min-w-10",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | function Toggle({
30 | className,
31 | variant,
32 | size,
33 | ...props
34 | }: React.ComponentProps &
35 | VariantProps) {
36 | return (
37 |
42 | )
43 | }
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/src/components/ui/badge.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 badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-semibold w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0 transition-[color,box-shadow]",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground shadow-sm [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-destructive-foreground shadow-sm [a&]:hover:bg-destructive/90",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Popover({
7 | ...props
8 | }: React.ComponentProps) {
9 | return
10 | }
11 |
12 | function PopoverTrigger({
13 | ...props
14 | }: React.ComponentProps) {
15 | return
16 | }
17 |
18 | function PopoverContent({
19 | className,
20 | align = "center",
21 | sideOffset = 4,
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
36 |
37 | )
38 | }
39 |
40 | function PopoverAnchor({
41 | ...props
42 | }: React.ComponentProps) {
43 | return
44 | }
45 |
46 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
47 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite"
2 | import react from "@vitejs/plugin-react-swc"
3 | import tailwindcss from "@tailwindcss/vite"
4 | import { lingui } from "@lingui/vite-plugin"
5 | import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
6 | import path from "path"
7 |
8 | const host = process.env.TAURI_DEV_HOST
9 |
10 | // https://vitejs.dev/config/
11 | export default defineConfig(async () => ({
12 | plugins: [
13 | react({
14 | plugins: [
15 | [
16 | "@lingui/swc-plugin",
17 | {
18 | // Optional
19 | // Unlike the JS version this option must be passed as object only.
20 | // Docs https://lingui.dev/ref/conf#runtimeconfigmodule
21 | runtimeModules: {
22 | i18n: ["@lingui/core", "i18n"],
23 | trans: ["@lingui/react", "Trans"],
24 | },
25 | },
26 | ],
27 | ],
28 | }),
29 | lingui(),
30 | tailwindcss(),
31 | TanStackRouterVite({ autoCodeSplitting: true }),
32 | ],
33 | resolve: {
34 | alias: {
35 | "@": path.resolve(__dirname, "./src"),
36 | },
37 | },
38 |
39 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
40 | //
41 | // 1. prevent vite from obscuring rust errors
42 | clearScreen: false,
43 | // 2. tauri expects a fixed port, fail if that port is not available
44 | server: {
45 | port: 1420,
46 | strictPort: true,
47 | host: host || false,
48 | hmr: host
49 | ? {
50 | protocol: "ws",
51 | host,
52 | port: 1421,
53 | }
54 | : undefined,
55 | watch: {
56 | // 3. tell vite to ignore watching `src-tauri`
57 | ignored: ["**/src-tauri/**"],
58 | },
59 | },
60 | }))
61 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
25 | )
26 | }
27 |
28 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
29 | return (
30 |
35 | )
36 | }
37 |
38 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
39 | return (
40 |
45 | )
46 | }
47 |
48 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
49 | return (
50 |
55 | )
56 | }
57 |
58 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
59 | return (
60 |
65 | )
66 | }
67 |
68 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
69 |
--------------------------------------------------------------------------------
/src/components/ReasoningDisplay.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogTrigger,
4 | DialogContent,
5 | DialogTitle,
6 | DialogDescription,
7 | } from "./ui/dialog"
8 | import { ChevronRight } from "lucide-react"
9 | import { Button } from "./ui/button"
10 | import { DialogHeader } from "./ui/dialog"
11 | import { ReactNode, useLayoutEffect, useRef } from "react"
12 |
13 | interface IReasoningDisplay {
14 | isReasoning: boolean
15 | buttonContent: ReactNode
16 | reasoningText: ReactNode
17 | title: ReactNode
18 | description: ReactNode
19 | }
20 | export default function ReasoningDisplay({
21 | isReasoning,
22 | buttonContent,
23 | title,
24 | description,
25 | reasoningText,
26 | }: IReasoningDisplay) {
27 | const reasoningTextRef = useRef(null)
28 |
29 | useLayoutEffect(() => {
30 | if (reasoningText && reasoningTextRef.current) {
31 | reasoningTextRef.current.scrollTop = reasoningTextRef.current.scrollHeight
32 | }
33 | }, [reasoningText])
34 |
35 | return (
36 |
37 |
38 |
42 | {buttonContent}
43 |
44 |
45 |
46 |
50 |
51 | {title}
52 | {description}
53 |
54 |
55 | {reasoningText}
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/buildSrc/src/main/java/xyz/lightm/taiga/app/kotlin/BuildTask.kt:
--------------------------------------------------------------------------------
1 | import java.io.File
2 | import org.apache.tools.ant.taskdefs.condition.Os
3 | import org.gradle.api.DefaultTask
4 | import org.gradle.api.GradleException
5 | import org.gradle.api.logging.LogLevel
6 | import org.gradle.api.tasks.Input
7 | import org.gradle.api.tasks.TaskAction
8 |
9 | open class BuildTask : DefaultTask() {
10 | @Input
11 | var rootDirRel: String? = null
12 | @Input
13 | var target: String? = null
14 | @Input
15 | var release: Boolean? = null
16 |
17 | @TaskAction
18 | fun assemble() {
19 | val executable = """yarn""";
20 | try {
21 | runTauriCli(executable)
22 | } catch (e: Exception) {
23 | if (Os.isFamily(Os.FAMILY_WINDOWS)) {
24 | runTauriCli("$executable.cmd")
25 | } else {
26 | throw e;
27 | }
28 | }
29 | }
30 |
31 | fun runTauriCli(executable: String) {
32 | val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
33 | val target = target ?: throw GradleException("target cannot be null")
34 | val release = release ?: throw GradleException("release cannot be null")
35 | val args = listOf("tauri", "android", "android-studio-script");
36 |
37 | project.exec {
38 | workingDir(File(project.projectDir, rootDirRel))
39 | executable(executable)
40 | args(args)
41 | if (project.logger.isEnabled(LogLevel.DEBUG)) {
42 | args("-vv")
43 | } else if (project.logger.isEnabled(LogLevel.INFO)) {
44 | args("-v")
45 | }
46 | if (release) {
47 | args("--release")
48 | }
49 | args(listOf("--target", target))
50 | }.assertNormalExitValue()
51 | }
52 | }
--------------------------------------------------------------------------------
/src/components/ui/alert.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 alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:text-destructive-foreground/80 dark:border-destructive [&>svg]:text-current dark:bg-destructive/50",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | function Alert({
23 | className,
24 | variant,
25 | ...props
26 | }: React.ComponentProps<"div"> & VariantProps) {
27 | return (
28 |
34 | )
35 | }
36 |
37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38 | return (
39 |
47 | )
48 | }
49 |
50 | function AlertDescription({
51 | className,
52 | ...props
53 | }: React.ComponentProps<"div">) {
54 | return (
55 |
63 | )
64 | }
65 |
66 | export { Alert, AlertTitle, AlertDescription }
67 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Tabs({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function TabsList({
20 | className,
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
32 | )
33 | }
34 |
35 | function TabsTrigger({
36 | className,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
48 | )
49 | }
50 |
51 | function TabsContent({
52 | className,
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
61 | )
62 | }
63 |
64 | export { Tabs, TabsList, TabsTrigger, TabsContent }
65 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 4,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
60 |
--------------------------------------------------------------------------------
/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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs 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 has-[>svg]:px-3",
25 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27 | icon: "size-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | function Button({
38 | className,
39 | variant,
40 | size,
41 | asChild = false,
42 | ...props
43 | }: React.ComponentProps<"button"> &
44 | VariantProps & {
45 | asChild?: boolean
46 | }) {
47 | const Comp = asChild ? Slot : "button"
48 |
49 | return (
50 |
55 | )
56 | }
57 |
58 | export { Button, buttonVariants }
59 |
--------------------------------------------------------------------------------
/public/preset-themes/taiga.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3 | font-size: 16px;
4 | line-height: 24px;
5 | font-weight: 400;
6 |
7 | color: #0f0f0f;
8 | background-color: #f6f6f6;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | --background: hsl(35 55 98%);
16 | --foreground: hsl(35 55 50%);
17 | --card: hsl(35 55 96%);
18 | --card-foreground: hsl(35 55 50%);
19 | --popover: hsl(35 55 96%);
20 | --popover-foreground: hsl(35 55 64%);
21 | --primary: hsl(35 55 50%);
22 | --primary-foreground: hsl(35 55 98%);
23 | --secondary: hsl(35 55 68%);
24 | --secondary-foreground: hsl(35 55 98%);
25 | --muted: hsl(35 55 88%);
26 | --muted-foreground: hsl(35 55 64%);
27 | --accent: hsl(35 55 88%);
28 | --accent-foreground: hsl(35 55 50%);
29 | --destructive: hsl(0 84.2% 60.2%);
30 | --destructive-foreground: hsl(0 0% 98%);
31 | --border: hsl(35 55 80%);
32 | --input: hsl(35 55 80%);
33 | --ring: hsl(35 55 56%);
34 | --chart-1: hsl(12 76% 61%);
35 | --chart-2: hsl(173 58% 39%);
36 | --chart-3: hsl(197 37% 24%);
37 | --chart-4: hsl(43 74% 66%);
38 | --chart-5: hsl(27 87% 67%);
39 | --radius: 0.6rem;
40 | }
41 |
42 | .dark {
43 | --background: hsl(0 0% 3.9%);
44 | --foreground: hsl(0 0% 98%);
45 | --card: hsl(0 0% 3.9%);
46 | --card-foreground: hsl(0 0% 98%);
47 | --popover: hsl(0 0% 3.9%);
48 | --popover-foreground: hsl(0 0% 98%);
49 | --primary: hsl(0 0% 98%);
50 | --primary-foreground: hsl(0 0% 9%);
51 | --secondary: hsl(0 0% 14.9%);
52 | --secondary-foreground: hsl(0 0% 98%);
53 | --muted: hsl(0 0% 14.9%);
54 | --muted-foreground: hsl(0 0% 63.9%);
55 | --accent: hsl(0 0% 14.9%);
56 | --accent-foreground: hsl(0 0% 98%);
57 | --destructive: hsl(0 62.8% 30.6%);
58 | --destructive-foreground: hsl(0 0% 98%);
59 | --border: hsl(0 0% 14.9%);
60 | --input: hsl(0 0% 14.9%);
61 | --ring: hsl(0 0% 83.1%);
62 | --chart-1: hsl(220 70% 50%);
63 | --chart-2: hsl(160 60% 45%);
64 | --chart-3: hsl(30 80% 55%);
65 | --chart-4: hsl(280 65% 60%);
66 | --chart-5: hsl(340 75% 55%);
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { GripVerticalIcon } from "lucide-react"
3 | import * as ResizablePrimitive from "react-resizable-panels"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function ResizablePanelGroup({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 | )
21 | }
22 |
23 | function ResizablePanel({
24 | ...props
25 | }: React.ComponentProps) {
26 | return
27 | }
28 |
29 | function ResizableHandle({
30 | withHandle,
31 | className,
32 | ...props
33 | }: React.ComponentProps & {
34 | withHandle?: boolean
35 | }) {
36 | return (
37 | div]:rotate-90",
41 | className
42 | )}
43 | {...props}
44 | >
45 | {withHandle && (
46 |
47 |
48 |
49 | )}
50 |
51 | )
52 | }
53 |
54 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
55 |
--------------------------------------------------------------------------------
/src/components/ScrollToBottom.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react"
2 | import { Button } from "./ui/button"
3 | import { ArrowDown } from "lucide-react"
4 |
5 | interface IScrollToBottom {
6 | observedRef: React.RefObject
7 | onClick?: () => void
8 | }
9 | export const SHOULD_RENDER_SCROLL_TO_BOTTOM_HEIGHT = 8
10 | export function ScrollToBottom({ observedRef, onClick }: IScrollToBottom) {
11 | const getShouldRender = useCallback(
12 | () =>
13 | observedRef.current
14 | ? Math.abs(
15 | observedRef.current.scrollHeight -
16 | observedRef.current.scrollTop -
17 | observedRef.current.clientHeight
18 | ) > SHOULD_RENDER_SCROLL_TO_BOTTOM_HEIGHT
19 | : false,
20 | [observedRef]
21 | )
22 |
23 | const [shouldRender, setShouldRender] = useState(getShouldRender)
24 |
25 | const scrollToBottom = useCallback(() => {
26 | if (observedRef.current) {
27 | const newScrollTop =
28 | observedRef.current.scrollHeight - observedRef.current.clientHeight
29 |
30 | observedRef.current.scrollTop = newScrollTop
31 | }
32 | if (onClick) onClick()
33 | }, [observedRef, onClick])
34 |
35 | useEffect(() => {
36 | if (observedRef.current) {
37 | const observedElement = observedRef.current as HTMLElement
38 |
39 | const handleScroll = () => {
40 | setShouldRender(getShouldRender())
41 | }
42 |
43 | const observer = new ResizeObserver(handleScroll)
44 | observer.observe(observedElement)
45 |
46 | const throttledHandleScroll = () => {
47 | requestAnimationFrame(handleScroll)
48 | }
49 |
50 | observedElement.addEventListener("scroll", throttledHandleScroll)
51 | return () => {
52 | observer.disconnect()
53 | observedElement.removeEventListener("scroll", throttledHandleScroll)
54 | }
55 | }
56 | }, [observedRef, getShouldRender])
57 |
58 | if (!shouldRender) {
59 | return null
60 | }
61 |
62 | return (
63 |
69 |
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/java/xyz/lightm/taiga/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.lightm.taiga.app
2 |
3 | import android.content.res.Configuration
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.view.View
7 | import androidx.core.view.WindowCompat
8 | import androidx.core.view.ViewCompat
9 | import androidx.core.view.WindowInsetsCompat
10 |
11 | class MainActivity : TauriActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | WindowCompat.setDecorFitsSystemWindows(window, false)
15 |
16 | ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, insets ->
17 | val systemBarHeight = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
18 | val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
19 |
20 | view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, if (imeHeight > 0) imeHeight else systemBarHeight)
21 |
22 | // Return the insets so other listeners can consume them too
23 | insets
24 | }
25 |
26 | setStatusBarTextColor()
27 | }
28 |
29 | override fun onConfigurationChanged(newConfig: Configuration) {
30 | super.onConfigurationChanged(newConfig)
31 | setStatusBarTextColor() // Reset status bar text color
32 | }
33 |
34 | private fun setStatusBarTextColor() {
35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
36 | val decorView = window.decorView
37 | var uiOptions = decorView.systemUiVisibility
38 | // Get whether the current theme is a dark theme
39 | val nightModeFlags = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
40 | val isDarkMode = nightModeFlags == Configuration.UI_MODE_NIGHT_YES
41 | if (isDarkMode) {
42 | // If it is dark mode, set light text (remove SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
43 | uiOptions = uiOptions and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
44 | } else {
45 | // If it is light mode, set dark text
46 | uiOptions = uiOptions or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
47 | }
48 | decorView.systemUiVisibility = uiOptions
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/components/NavigationDescription.tsx:
--------------------------------------------------------------------------------
1 | import { t } from "@lingui/core/macro"
2 | import { openUrl } from "@tauri-apps/plugin-opener"
3 |
4 | import { PRESET_NAMES } from "@/lib/updater"
5 | import { FormDescription } from "./ui/form"
6 |
7 | interface INavigationDescription {
8 | providerName: string | undefined
9 | type: "search_api_key" | "provider_api_key" | "search_api_url" | "customize_theme"
10 | }
11 |
12 | export function NavigationDescription({
13 | providerName,
14 | type,
15 | }: INavigationDescription) {
16 | let url: string
17 | let customDescription: string | undefined
18 |
19 | switch (type) {
20 | case "provider_api_key": {
21 | switch (providerName) {
22 | case PRESET_NAMES.DEEPSEEK_PRESET:
23 | url = "https://platform.deepseek.com/api_keys"
24 | break
25 | case PRESET_NAMES.GOOGLE_PRESET:
26 | url = "https://aistudio.google.com/apikey"
27 | break
28 | case PRESET_NAMES.OPENAI_PRESET:
29 | url = "https://platform.openai.com/docs/overview"
30 | break
31 | case PRESET_NAMES.OPENROUTER_PRESET:
32 | url = "https://openrouter.ai/settings/keys"
33 | break
34 | default:
35 | return null
36 | }
37 | break
38 | }
39 | case "search_api_url": {
40 | switch (providerName) {
41 | case PRESET_NAMES.SEARXNG_PRESET:
42 | url = "https://searx.space/"
43 | customDescription = t`Find more Searxng instances`
44 | break
45 | default:
46 | return null
47 | }
48 | break
49 | }
50 | case "search_api_key": {
51 | switch (providerName) {
52 | case PRESET_NAMES.JINA_PRESET:
53 | url = "https://jina.ai/api-dashboard/key-manager"
54 | break
55 | default:
56 | return null
57 | }
58 | break
59 | }
60 | case "customize_theme": {
61 | url = "https://tweakcn.com"
62 | customDescription = t`Customize your own theme from tweakcn`
63 | break
64 | }
65 | }
66 |
67 | const getApiKeyString = customDescription ?? t`Get API keys`
68 |
69 | return (
70 |
71 | {
74 | openUrl(url)
75 | }}
76 | className="text-blue-500"
77 | >
78 | {getApiKeyString}
79 |
80 |
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/public/tauri.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Refer to https://github.com/vercel/ai-chatbot/blob/27ff56625848f091b40c65506858c146f742a047/components/code-block.tsx
4 | import { ComponentProps, useCallback } from "react"
5 | import { useIsDark } from "@/hooks/useIsDark"
6 | import { t } from "@lingui/core/macro"
7 | import { debounce } from "lodash-es"
8 | import { Copy } from "lucide-react"
9 | import { ExtraProps } from "react-markdown"
10 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
11 | import {
12 | atomDark as dark,
13 | oneLight as light,
14 | } from "react-syntax-highlighter/dist/esm/styles/prism"
15 | import { toast } from "sonner"
16 |
17 | import { Button } from "./ui/button"
18 |
19 | export function PreBlock({
20 | node,
21 | children,
22 | }: ComponentProps<"pre"> & ExtraProps) {
23 | const copyCode = useCallback(
24 | debounce(() => {
25 | const codeNode = node?.children.find(
26 | (c) => c.type === "element" && c.tagName === "code"
27 | )
28 | const codeContent =
29 | codeNode?.type === "element"
30 | ? codeNode.children.find((c) => c.type === "text")?.value
31 | : undefined
32 |
33 | if (codeContent) {
34 | navigator.clipboard.writeText(codeContent)
35 | toast.success(t`Copied to clipboard`)
36 | }
37 | }, 500),
38 | [children]
39 | )
40 |
41 | return (
42 |
43 |
49 |
50 |
51 | {children}
52 |
53 | )
54 | }
55 |
56 | export function CodeBlock({
57 | node,
58 | className,
59 | children,
60 | ...props
61 | }: ComponentProps<"code"> & ExtraProps) {
62 | const isDark = useIsDark()
63 | const inline = node?.position?.start.line === node?.position?.end.line
64 |
65 | const match = /language-(\w+)/.exec(className || "")
66 |
67 | return inline ? (
68 |
72 | {children}
73 |
74 | ) : (
75 | // @ts-ignore
76 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/src/lib/search-ai-tools.ts:
--------------------------------------------------------------------------------
1 | import { AIFunctionSet } from "@agentic/core"
2 | // import { ExaClient } from "@agentic/exa"
3 | import { JinaClient } from "@agentic/jina"
4 | // import { SearxngClient } from "@agentic/searxng"
5 | // import { fetch as tFetch } from "@tauri-apps/plugin-http"
6 | import ky from "ky"
7 |
8 | import { PRESET_NAMES } from "./updater"
9 |
10 | // [NOTE] Pause Exa Integration as CORS issues and a lot models not support schemas well
11 | // [NOTE] Pause Searxng Integration as no way to avoid limiter of searxng instance now
12 | export function getSearchAITools(apiKeys: Record) {
13 | const defaultKy = ky.extend({ timeout: 60_000 })
14 |
15 | return new AIFunctionSet(
16 | [
17 | // apiKeys[PRESET_NAMES.SEARXNG_PRESET]
18 | // ? new SearxngClient({
19 | // ky: defaultKy.extend({
20 | // fetch: tFetch,
21 | // headers: {
22 | // Accept:
23 | // "application/json,text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
24 | // "User-Agent":
25 | // "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
26 | // "Accept-Encoding": "gzip, deflate, br, zstd",
27 | // "Accept-Language": "en-US,en;q=0.9",
28 | // "Cache-Control": "no-cache",
29 | // "Sec-Ch-Ua": `"Not(A:Brand";v="99", "Chromium";v="133", "Google Chrome";v="133"`,
30 | // "Sec-Fetch-Dest": "document",
31 | // "Sec-Fetch-Mode": "navigate",
32 | // "Sec-Fetch-Site": "none",
33 | // "Sec-Fetch-User": "?1",
34 | // Pragma: "no-cache",
35 | // Priority: "u=0, i",
36 | // "Upgrade-Insecure-Requests": "1",
37 | // },
38 | // }),
39 | // apiBaseUrl: apiKeys[PRESET_NAMES.SEARXNG_PRESET],
40 | // })
41 | // : null,
42 | apiKeys[PRESET_NAMES.JINA_PRESET]
43 | ? new JinaClient({
44 | ky: defaultKy.extend({
45 | hooks: {
46 | beforeRequest: [
47 | (rq) => {
48 | rq.headers.set("Accept", "application/json")
49 | rq.headers.set("X-With-Favicons", "true")
50 | },
51 | ],
52 | },
53 | }),
54 | apiKey: apiKeys[PRESET_NAMES.JINA_PRESET],
55 | })
56 | : null,
57 | // apiKeys["Exa[Preset]"]
58 | // ? new ExaClient({ ky: defaultKy, apiKey: apiKeys["Exa[Preset]"] || "" })
59 | // : null,
60 | ].filter((t) => t !== null)
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 | import java.io.FileInputStream
3 |
4 | plugins {
5 | id("com.android.application")
6 | id("org.jetbrains.kotlin.android")
7 | id("rust")
8 | }
9 |
10 | val tauriProperties = Properties().apply {
11 | val propFile = file("tauri.properties")
12 | if (propFile.exists()) {
13 | propFile.inputStream().use { load(it) }
14 | }
15 | }
16 |
17 | android {
18 | compileSdk = 34
19 | namespace = "xyz.lightm.taiga.app"
20 | defaultConfig {
21 | manifestPlaceholders["usesCleartextTraffic"] = "false"
22 | applicationId = "xyz.lightm.taiga.app"
23 | minSdk = 24
24 | targetSdk = 34
25 | versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
26 | versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
27 | }
28 | // Refer to https://v2.tauri.app/distribute/sign/android/
29 | signingConfigs {
30 | create("release") {
31 | val keystorePropertiesFile = rootProject.file("keystore.properties")
32 | val keystoreProperties = Properties()
33 | if (keystorePropertiesFile.exists()) {
34 | keystoreProperties.load(FileInputStream(keystorePropertiesFile))
35 | }
36 |
37 | keyAlias = keystoreProperties["keyAlias"] as String
38 | keyPassword = keystoreProperties["password"] as String
39 | storeFile = file(keystoreProperties["storeFile"] as String)
40 | storePassword = keystoreProperties["password"] as String
41 | }
42 | }
43 | buildTypes {
44 | getByName("debug") {
45 | manifestPlaceholders["usesCleartextTraffic"] = "true"
46 | isDebuggable = true
47 | isJniDebuggable = true
48 | isMinifyEnabled = false
49 | packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
50 | jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
51 | jniLibs.keepDebugSymbols.add("*/x86/*.so")
52 | jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
53 | }
54 | }
55 | getByName("release") {
56 | signingConfig = signingConfigs.getByName("release")
57 | isMinifyEnabled = true
58 | proguardFiles(
59 | *fileTree(".") { include("**/*.pro") }
60 | .plus(getDefaultProguardFile("proguard-android-optimize.txt"))
61 | .toList().toTypedArray()
62 | )
63 | }
64 | }
65 | kotlinOptions {
66 | jvmTarget = "1.8"
67 | }
68 | buildFeatures {
69 | buildConfig = true
70 | }
71 | }
72 |
73 | rust {
74 | rootDirRel = "../../../"
75 | }
76 |
77 | dependencies {
78 | implementation("androidx.webkit:webkit:1.6.1")
79 | implementation("androidx.appcompat:appcompat:1.6.1")
80 | implementation("com.google.android.material:material:1.8.0")
81 | testImplementation("junit:junit:4.13.2")
82 | androidTestImplementation("androidx.test.ext:junit:1.1.4")
83 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
84 | }
85 |
86 | apply(from = "tauri.build.gradle.kts")
--------------------------------------------------------------------------------
/src/components/ToolCallDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useLayoutEffect, useMemo, useRef } from "react"
2 | import { SearchReturnSchema, TSearchReturnType } from "@/hooks/useChat"
3 | import { openUrl } from "@tauri-apps/plugin-opener"
4 | import { ChevronRight } from "lucide-react"
5 |
6 | import { Button } from "./ui/button"
7 | import {
8 | Dialog,
9 | DialogContent,
10 | DialogDescription,
11 | DialogHeader,
12 | DialogTitle,
13 | DialogTrigger,
14 | } from "./ui/dialog"
15 |
16 | interface IToolCallDisplay {
17 | isCalling: boolean
18 | buttonContent: ReactNode
19 | toolCallResult: any
20 | title: ReactNode
21 | description: ReactNode
22 | }
23 |
24 | // [TODO] Support Exa Tool Calling Result Display
25 | export default function ToolCallDisplay({
26 | isCalling,
27 | buttonContent,
28 | title,
29 | description,
30 | toolCallResult,
31 | }: IToolCallDisplay) {
32 | const toolCallResultRef = useRef(null)
33 |
34 | useLayoutEffect(() => {
35 | if (toolCallResult && toolCallResultRef.current) {
36 | toolCallResultRef.current.scrollTop =
37 | toolCallResultRef.current.scrollHeight
38 | }
39 | }, [toolCallResult])
40 |
41 | const result = useMemo(() => {
42 | if (
43 | typeof toolCallResult === "string" ||
44 | typeof toolCallResult === "number"
45 | ) {
46 | return toolCallResult
47 | } else if (SearchReturnSchema.safeParse(toolCallResult).success) {
48 | return (toolCallResult as TSearchReturnType).data.map(
49 | ({ url, title, description, favicon, usage: { tokens } }, index) => {
50 | return (
51 | {
55 | await openUrl(url)
56 | }}
57 | >
58 |
59 | {favicon ? : null}
60 | {title}
61 |
62 |
{description}
63 |
64 | Used token: {tokens.toString()}
65 |
66 |
67 | )
68 | }
69 | )
70 | }
71 | }, [toolCallResult])
72 |
73 | return (
74 |
75 |
76 |
80 | {buttonContent}
81 |
82 |
83 |
84 |
88 |
89 | {title}
90 | {description}
91 |
92 |
96 | {result}
97 |
98 |
99 |
100 | )
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/Markdown.tsx:
--------------------------------------------------------------------------------
1 | // Refer to https://github.com/vercel/ai-chatbot/blob/27ff56625848f091b40c65506858c146f742a047/components/markdown.tsx
2 |
3 | import { memo } from "react"
4 | import { openUrl } from "@tauri-apps/plugin-opener"
5 | import ReactMarkdown, { Options, type Components } from "react-markdown"
6 | import rehypeKatex from "rehype-katex"
7 | import remarkGfm from "remark-gfm"
8 | import remarkMath from "remark-math"
9 |
10 | import { CodeBlock, PreBlock } from "./CodeBlock"
11 |
12 | import "katex/dist/katex.min.css"
13 |
14 | const components: Partial = {
15 | code: CodeBlock,
16 | pre: PreBlock,
17 | ol: ({ node, children, ...props }) => {
18 | return (
19 |
20 | {children}
21 |
22 | )
23 | },
24 | li: ({ node, children, ...props }) => {
25 | return (
26 |
27 | {children}
28 |
29 | )
30 | },
31 | ul: ({ node, children, ...props }) => {
32 | return (
33 |
36 | )
37 | },
38 | strong: ({ node, children, ...props }) => {
39 | return (
40 |
41 | {children}
42 |
43 | )
44 | },
45 | a: ({ node, children, href, ...props }) => {
46 | return (
47 | {
52 | if (href) {
53 | openUrl(href)
54 | }
55 | }}
56 | {...props}
57 | >
58 | {children}
59 |
60 | )
61 | },
62 | h1: ({ node, children, ...props }) => {
63 | return (
64 |
65 | {children}
66 |
67 | )
68 | },
69 | h2: ({ node, children, ...props }) => {
70 | return (
71 |
72 | {children}
73 |
74 | )
75 | },
76 | h3: ({ node, children, ...props }) => {
77 | return (
78 |
79 | {children}
80 |
81 | )
82 | },
83 | h4: ({ node, children, ...props }) => {
84 | return (
85 |
86 | {children}
87 |
88 | )
89 | },
90 | h5: ({ node, children, ...props }) => {
91 | return (
92 |
93 | {children}
94 |
95 | )
96 | },
97 | h6: ({ node, children, ...props }) => {
98 | return (
99 |
100 | {children}
101 |
102 | )
103 | },
104 | hr: ({ node, children, ...props }) => {
105 | return
106 | },
107 | }
108 |
109 | const remarkPlugins = [remarkGfm, remarkMath]
110 | const rehypePlugins = [rehypeKatex]
111 |
112 | const NonMemoizedMarkdown = ({ children }: Pick) => {
113 | return (
114 |
115 | {children}
116 |
117 | )
118 | }
119 |
120 | export const Markdown = memo(
121 | NonMemoizedMarkdown,
122 | (prevProps, nextProps) => prevProps.children === nextProps.children
123 | )
124 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/buildSrc/src/main/java/xyz/lightm/taiga/app/kotlin/RustPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ApplicationExtension
2 | import org.gradle.api.DefaultTask
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.configure
6 | import org.gradle.kotlin.dsl.get
7 |
8 | const val TASK_GROUP = "rust"
9 |
10 | open class Config {
11 | lateinit var rootDirRel: String
12 | }
13 |
14 | open class RustPlugin : Plugin {
15 | private lateinit var config: Config
16 |
17 | override fun apply(project: Project) = with(project) {
18 | config = extensions.create("rust", Config::class.java)
19 |
20 | val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
21 | val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
22 |
23 | val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
24 | val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
25 |
26 | val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
27 |
28 | extensions.configure {
29 | @Suppress("UnstableApiUsage")
30 | flavorDimensions.add("abi")
31 | productFlavors {
32 | create("universal") {
33 | dimension = "abi"
34 | ndk {
35 | abiFilters += abiList
36 | }
37 | }
38 | defaultArchList.forEachIndexed { index, arch ->
39 | create(arch) {
40 | dimension = "abi"
41 | ndk {
42 | abiFilters.add(defaultAbiList[index])
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | afterEvaluate {
50 | for (profile in listOf("debug", "release")) {
51 | val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
52 | val buildTask = tasks.maybeCreate(
53 | "rustBuildUniversal$profileCapitalized",
54 | DefaultTask::class.java
55 | ).apply {
56 | group = TASK_GROUP
57 | description = "Build dynamic library in $profile mode for all targets"
58 | }
59 |
60 | tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
61 |
62 | for (targetPair in targetsList.withIndex()) {
63 | val targetName = targetPair.value
64 | val targetArch = archList[targetPair.index]
65 | val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
66 | val targetBuildTask = project.tasks.maybeCreate(
67 | "rustBuild$targetArchCapitalized$profileCapitalized",
68 | BuildTask::class.java
69 | ).apply {
70 | group = TASK_GROUP
71 | description = "Build dynamic library in $profile mode for $targetArch"
72 | rootDirRel = config.rootDirRel
73 | target = targetName
74 | release = profile == "release"
75 | }
76 |
77 | buildTask.dependsOn(targetBuildTask)
78 | tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
79 | targetBuildTask
80 | )
81 | }
82 | }
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "taiga",
3 | "private": true,
4 | "version": "0.1.0-rc.10",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview",
10 | "tauri": "tauri",
11 | "extract": "lingui extract",
12 | "compile": "lingui compile"
13 | },
14 | "resolutions": {
15 | "@swc/core": "1.9.0"
16 | },
17 | "dependencies": {
18 | "@agentic/ai-sdk": "^7.3.3",
19 | "@agentic/calculator": "^7.3.3",
20 | "@agentic/exa": "^7.3.3",
21 | "@agentic/jina": "^7.3.4",
22 | "@agentic/searxng": "^7.3.3",
23 | "@ai-sdk/deepseek": "^0.1.12",
24 | "@ai-sdk/google": "^1.1.17",
25 | "@ai-sdk/openai": "^1.1.14",
26 | "@ai-sdk/openai-compatible": "^0.1.12",
27 | "@ai-sdk/perplexity": "^1.1.9",
28 | "@hookform/resolvers": "^4.0.0",
29 | "@icons-pack/react-simple-icons": "^12.1.0",
30 | "@lingui/core": "^5.2.0",
31 | "@lingui/react": "^5.2.0",
32 | "@openrouter/ai-sdk-provider": "^0.4.1",
33 | "@radix-ui/react-alert-dialog": "^1.1.6",
34 | "@radix-ui/react-collapsible": "^1.1.3",
35 | "@radix-ui/react-context-menu": "^2.2.6",
36 | "@radix-ui/react-dialog": "^1.1.6",
37 | "@radix-ui/react-dropdown-menu": "^2.1.6",
38 | "@radix-ui/react-label": "^2.1.2",
39 | "@radix-ui/react-popover": "^1.1.6",
40 | "@radix-ui/react-select": "^2.1.6",
41 | "@radix-ui/react-slot": "^1.1.2",
42 | "@radix-ui/react-switch": "^1.1.3",
43 | "@radix-ui/react-tabs": "^1.1.3",
44 | "@radix-ui/react-toggle": "^1.1.2",
45 | "@radix-ui/react-tooltip": "^1.1.8",
46 | "@tailwindcss/vite": "^4.0.6",
47 | "@tanstack/react-router": "^1.104.1",
48 | "@tauri-apps/api": "^2",
49 | "@tauri-apps/plugin-fs": "~2",
50 | "@tauri-apps/plugin-http": "~2",
51 | "@tauri-apps/plugin-opener": "~2",
52 | "@tauri-apps/plugin-store": "~2",
53 | "ai": "^4.3.16",
54 | "class-variance-authority": "^0.7.1",
55 | "clsx": "^2.1.1",
56 | "cmdk": "1.0.0",
57 | "dayjs": "^1.11.13",
58 | "immer": "^10.1.1",
59 | "katex": "^0.16.22",
60 | "lodash-es": "^4.17.21",
61 | "lucide-react": "^0.475.0",
62 | "next-themes": "^0.4.4",
63 | "ogl": "^1.0.11",
64 | "react": "19",
65 | "react-dom": "19",
66 | "react-hook-form": "^7.54.2",
67 | "react-markdown": "^9.0.3",
68 | "react-resizable-panels": "^2.1.7",
69 | "react-syntax-highlighter": "^15.6.1",
70 | "rehype-katex": "^7.0.1",
71 | "remark-gfm": "^4.0.1",
72 | "remark-math": "^6.0.0",
73 | "sonner": "^1.7.4",
74 | "tailwind-merge": "^3.0.1",
75 | "tailwindcss": "^4.0.6",
76 | "tailwindcss-animate": "^1.0.7",
77 | "vaul": "^1.1.2",
78 | "zod": "^3.24.2"
79 | },
80 | "devDependencies": {
81 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
82 | "@lingui/cli": "^5.2.0",
83 | "@lingui/swc-plugin": "^5.1.0",
84 | "@lingui/vite-plugin": "^5.2.0",
85 | "@tanstack/router-devtools": "^1.104.3",
86 | "@tanstack/router-plugin": "^1.104.1",
87 | "@tauri-apps/cli": "^2",
88 | "@types/lodash-es": "^4.17.12",
89 | "@types/node": "^22.13.2",
90 | "@types/react": "19",
91 | "@types/react-dom": "19",
92 | "@types/react-syntax-highlighter": "^15.5.13",
93 | "@typescript-eslint/parser": "^8.24.0",
94 | "@vitejs/plugin-react": "^4.3.4",
95 | "@vitejs/plugin-react-swc": "^3.8.0",
96 | "eslint": "^8.0.1",
97 | "eslint-plugin-import": "^2.25.2",
98 | "eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
99 | "eslint-plugin-promise": "^6.0.0",
100 | "eslint-plugin-react-hooks": "^5.1.0",
101 | "eslint-plugin-tailwindcss": "^3.18.0",
102 | "typescript": "~5.6.2",
103 | "vite": "^6.0.3"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/AIProvidersContext.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | PropsWithChildren,
4 | useCallback,
5 | useMemo,
6 | useState,
7 | } from "react"
8 | import { produce } from "immer"
9 |
10 | import { AIProviderManager, IAIProvider } from "@/lib/ai-providers"
11 | import {
12 | DefaultModelStore,
13 | TDefaultModels,
14 | } from "@/lib/default-model-store"
15 |
16 | export default function AIProviderContextProvider({
17 | children,
18 | }: PropsWithChildren) {
19 | const context = useAIProviderContext()
20 |
21 | return (
22 |
23 | {children}
24 |
25 | )
26 | }
27 |
28 | export type TAIProviderContextType = ReturnType
29 | export const AIProviderContext = createContext({
30 | providers: [],
31 | allProviders: [],
32 | defaultModels: {
33 | translate: null,
34 | chat: null,
35 | summarize: null,
36 | },
37 | selectedModel: null,
38 | isLoadingProviders: false,
39 | setDefaultModel: async () => {},
40 | setSelectedModel: () => {},
41 | fetchProviders: (async () => []) as () => Promise,
42 | fetchDefaultModels: (async () => ({})) as () => Promise,
43 | })
44 |
45 | function useAIProviderContext() {
46 | const [allProviders, setAllProviders] = useState([])
47 | /** Selected model is a tuple of [providerId, modelId] */
48 | const [selectedModel, setSelectedModel] = useState<[string, string] | null>(
49 | null
50 | )
51 | const [defaultModels, setDefaultModels] = useState({
52 | translate: null,
53 | chat: null,
54 | summarize: null,
55 | })
56 | const [isLoadingProviders, setIsLoadingProviders] = useState(false)
57 |
58 | const providers = useMemo(
59 | () => allProviders.filter((p) => p.models.length > 0),
60 | [allProviders]
61 | )
62 |
63 | const fetchProviders = useCallback(async () => {
64 | setIsLoadingProviders(true)
65 | const _providers = await AIProviderManager.loadProviders()
66 | setIsLoadingProviders(false)
67 | setAllProviders(_providers)
68 |
69 | return _providers
70 | }, [])
71 |
72 | const fetchDefaultModels = useCallback(async () => {
73 | const _defaultModels = await DefaultModelStore.getDefaultModels()
74 | const newProviders = await fetchProviders()
75 |
76 | const validDefaultModels = produce(_defaultModels, (draft) => {
77 | Object.entries(draft)
78 | .filter(([_, model]) => model !== null)
79 | .forEach(async ([type, model]) => {
80 | let found = false
81 | for (const provider of newProviders) {
82 | if (provider.name === model![0]) {
83 | if (provider.models.includes(model![1])) {
84 | found = true
85 | break
86 | } else {
87 | break
88 | }
89 | }
90 | }
91 | if (!found) {
92 | draft[type as keyof TDefaultModels] = null
93 |
94 | // effect
95 | await DefaultModelStore.setDefaultModel(
96 | type as keyof TDefaultModels,
97 | null
98 | )
99 | }
100 | })
101 | })
102 |
103 | setDefaultModels(validDefaultModels)
104 |
105 | return validDefaultModels
106 | }, [fetchProviders])
107 |
108 | const setDefaultModel = useCallback(
109 | async (type, model) => {
110 | await DefaultModelStore.setDefaultModel(type, model)
111 | await fetchDefaultModels()
112 | },
113 | [fetchDefaultModels]
114 | )
115 |
116 | return {
117 | providers,
118 | allProviders,
119 | selectedModel,
120 | defaultModels,
121 | isLoadingProviders,
122 | setDefaultModel,
123 | setSelectedModel,
124 | fetchProviders,
125 | fetchDefaultModels,
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/lib/github-get-latest-release-response-schema.d.ts:
--------------------------------------------------------------------------------
1 | // json-schema to d.ts https://borischerny.com/json-schema-to-typescript-browser
2 | // github api https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release
3 |
4 | /* eslint-disable */
5 | /**
6 | * This file was automatically generated by json-schema-to-typescript.
7 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
8 | * and run json-schema-to-typescript to regenerate this file.
9 | */
10 |
11 | /**
12 | * A release.
13 | */
14 | export namespace GithubAPI {
15 | export interface Release {
16 | url: string
17 | html_url: string
18 | assets_url: string
19 | upload_url: string
20 | tarball_url: string | null
21 | zipball_url: string | null
22 | id: number
23 | node_id: string
24 | /**
25 | * The name of the tag.
26 | */
27 | tag_name: string
28 | /**
29 | * Specifies the commitish value that determines where the Git tag is created from.
30 | */
31 | target_commitish: string
32 | name: string | null
33 | body?: string | null
34 | /**
35 | * true to create a draft (unpublished) release, false to create a published one.
36 | */
37 | draft: boolean
38 | /**
39 | * Whether to identify the release as a prerelease or a full release.
40 | */
41 | prerelease: boolean
42 | created_at: string
43 | published_at: string | null
44 | author: SimpleUser
45 | assets: ReleaseAsset[]
46 | body_html?: string
47 | body_text?: string
48 | mentions_count?: number
49 | /**
50 | * The URL of the release discussion.
51 | */
52 | discussion_url?: string
53 | reactions?: ReactionRollup
54 | [k: string]: unknown
55 | }
56 | /**
57 | * A GitHub user.
58 | */
59 | export interface SimpleUser {
60 | name?: string | null
61 | email?: string | null
62 | login: string
63 | id: number
64 | node_id: string
65 | avatar_url: string
66 | gravatar_id: string | null
67 | url: string
68 | html_url: string
69 | followers_url: string
70 | following_url: string
71 | gists_url: string
72 | starred_url: string
73 | subscriptions_url: string
74 | organizations_url: string
75 | repos_url: string
76 | events_url: string
77 | received_events_url: string
78 | type: string
79 | site_admin: boolean
80 | starred_at?: string
81 | user_view_type?: string
82 | [k: string]: unknown
83 | }
84 | /**
85 | * Data related to a release.
86 | */
87 | export interface ReleaseAsset {
88 | url: string
89 | browser_download_url: string
90 | id: number
91 | node_id: string
92 | /**
93 | * The file name of the asset.
94 | */
95 | name: string
96 | label: string | null
97 | /**
98 | * State of the release asset.
99 | */
100 | state: "uploaded" | "open"
101 | content_type: string
102 | size: number
103 | download_count: number
104 | created_at: string
105 | updated_at: string
106 | uploader: null | SimpleUser1
107 | [k: string]: unknown
108 | }
109 | /**
110 | * A GitHub user.
111 | */
112 | export interface SimpleUser1 {
113 | name?: string | null
114 | email?: string | null
115 | login: string
116 | id: number
117 | node_id: string
118 | avatar_url: string
119 | gravatar_id: string | null
120 | url: string
121 | html_url: string
122 | followers_url: string
123 | following_url: string
124 | gists_url: string
125 | starred_url: string
126 | subscriptions_url: string
127 | organizations_url: string
128 | repos_url: string
129 | events_url: string
130 | received_events_url: string
131 | type: string
132 | site_admin: boolean
133 | starred_at?: string
134 | user_view_type?: string
135 | [k: string]: unknown
136 | }
137 | export interface ReactionRollup {
138 | url: string
139 | total_count: number
140 | "+1": number
141 | "-1": number
142 | laugh: number
143 | confused: number
144 | heart: number
145 | hooray: number
146 | eyes: number
147 | rocket: number
148 | [k: string]: unknown
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/lib/chat-store.ts:
--------------------------------------------------------------------------------
1 | import { TExpandedMessage } from "@/hooks/useChat"
2 | import { load } from "@tauri-apps/plugin-store"
3 |
4 | const chatStoreKey = "chats.json"
5 | const chatSummaryStoreKey = "chat-summaries.json"
6 | const lastChatIdStoreKey = "last-chat-id.json"
7 |
8 | export type TChatID = ReturnType
9 | export type TChat = {
10 | id: TChatID
11 | summary: string
12 | createdAt: number
13 | modifiedAt: number
14 | nodes: TChatNode[]
15 | }
16 | export type TChatNode = {
17 | message: TExpandedMessage
18 | children: TChatNode[]
19 | }
20 |
21 | export class ChatStore {
22 | static async createOrUpdateChat({
23 | id,
24 | summary,
25 | editTime,
26 | nodes,
27 | }: Pick & { editTime: number }) {
28 | const store = await load(chatStoreKey)
29 |
30 | await ChatSummaryStore.createOrUpdateSummary({
31 | id,
32 | editTime,
33 | summary,
34 | })
35 | const chatIns = await store.get(id)
36 | if (chatIns) {
37 | await store.set(id, {
38 | ...chatIns,
39 | summary,
40 | modifiedAt: editTime,
41 | nodes,
42 | })
43 | } else {
44 | await store.set(id, {
45 | id,
46 | createdAt: editTime,
47 | modifiedAt: editTime,
48 | summary,
49 | nodes,
50 | })
51 | }
52 | }
53 |
54 | static async getChat(id: string) {
55 | const store = await load(chatStoreKey)
56 | const chat = await store.get(id)
57 | if (!chat) {
58 | throw new Error(`Chat with id ${id} not found`)
59 | }
60 | return chat as TChat
61 | }
62 |
63 | static async deleteChat(id: string) {
64 | const store = await load(chatStoreKey)
65 | if (await store.has(id)) {
66 | await ChatSummaryStore.deleteSummary(id)
67 | await store.delete(id)
68 | } else {
69 | throw new Error(`Chat with id ${id} not found`)
70 | }
71 | }
72 |
73 | static async clearAllChats() {
74 | const store = await load(chatStoreKey)
75 | await store.clear()
76 | await ChatSummaryStore.clearAllSummaries()
77 | }
78 | }
79 |
80 | export type TChatSummary = {
81 | id: TChatID
82 | summary: string
83 | createdAt: number
84 | modifiedAt: number
85 | }
86 | export class ChatSummaryStore {
87 | static async createOrUpdateSummary({
88 | id,
89 | editTime,
90 | summary,
91 | }: Pick & { editTime: number }) {
92 | const store = await load(chatSummaryStoreKey)
93 |
94 | const summaryIns = await store.get(id)
95 | if (summaryIns) {
96 | await store.set(id, {
97 | ...summaryIns,
98 | summary,
99 | modifiedAt: editTime,
100 | })
101 | } else {
102 | await store.set(id, {
103 | id,
104 | createdAt: editTime,
105 | modifiedAt: editTime,
106 | summary,
107 | })
108 | }
109 | }
110 |
111 | static async getSummary(id: string) {
112 | const store = await load(chatSummaryStoreKey)
113 | const summary = await store.get(id)
114 | if (!summary) {
115 | throw new Error(`Summary for chat with id ${id} not found`)
116 | }
117 | return summary as TChatSummary
118 | }
119 |
120 | static async getAllSummaries() {
121 | const store = await load(chatSummaryStoreKey)
122 | const summaries = await store.values()
123 | return summaries as TChatSummary[]
124 | }
125 |
126 | static async deleteSummary(id: string) {
127 | const store = await load(chatSummaryStoreKey)
128 | if (await store.has(id)) {
129 | await store.delete(id)
130 | } else {
131 | throw new Error(`Summary for chat with id ${id} not found`)
132 | }
133 | }
134 |
135 | static async clearAllSummaries() {
136 | const store = await load(chatSummaryStoreKey)
137 | await store.clear()
138 | }
139 | }
140 |
141 | export class LastChatIDStore {
142 | static async getLastChatID() {
143 | const store = await load(lastChatIdStoreKey)
144 | const lastChatID = await store.get("lastChatID")
145 |
146 | return lastChatID as TChatID
147 | }
148 |
149 | static async setLastChatID(id: TChatID) {
150 | const store = await load(lastChatIdStoreKey)
151 | await store.set("lastChatID", id)
152 | }
153 | }
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Drawer as DrawerPrimitive } from "vaul"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Drawer({
7 | ...props
8 | }: React.ComponentProps) {
9 | return
10 | }
11 |
12 | function DrawerTrigger({
13 | ...props
14 | }: React.ComponentProps) {
15 | return
16 | }
17 |
18 | function DrawerPortal({
19 | ...props
20 | }: React.ComponentProps) {
21 | return
22 | }
23 |
24 | function DrawerClose({
25 | ...props
26 | }: React.ComponentProps) {
27 | return
28 | }
29 |
30 | function DrawerOverlay({
31 | className,
32 | ...props
33 | }: React.ComponentProps) {
34 | return (
35 |
43 | )
44 | }
45 |
46 | function DrawerContent({
47 | className,
48 | children,
49 | ...props
50 | }: React.ComponentProps) {
51 | return (
52 |
53 |
54 |
66 |
67 | {children}
68 |
69 |
70 | )
71 | }
72 |
73 | function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
74 | return (
75 |
80 | )
81 | }
82 |
83 | function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
84 | return (
85 |
90 | )
91 | }
92 |
93 | function DrawerTitle({
94 | className,
95 | ...props
96 | }: React.ComponentProps) {
97 | return (
98 |
103 | )
104 | }
105 |
106 | function DrawerDescription({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
116 | )
117 | }
118 |
119 | export {
120 | Drawer,
121 | DrawerPortal,
122 | DrawerOverlay,
123 | DrawerTrigger,
124 | DrawerClose,
125 | DrawerContent,
126 | DrawerHeader,
127 | DrawerFooter,
128 | DrawerTitle,
129 | DrawerDescription,
130 | }
131 |
--------------------------------------------------------------------------------
/src/lib/updater.ts:
--------------------------------------------------------------------------------
1 | import { load } from "@tauri-apps/plugin-store"
2 |
3 | import { AIProviderManager, IAIProvider } from "./ai-providers"
4 | import { GithubAPI } from "./github-get-latest-release-response-schema"
5 | import { SearchApiStore, TBaseSearchAPI } from "./search-api-store"
6 | import { FONT_SIZE_STORAGE_KEY, FONT_WEIGHT_STORAGE_KEY, FontSize, FontWeight } from "@/components/ThemeProvider"
7 |
8 | const VERSION_TAGS_STORE_KEY = "VERSIONs.json"
9 |
10 | type TUpdater = [string, () => Promise]
11 | async function excute([tag, updater]: TUpdater) {
12 | const store = await load(VERSION_TAGS_STORE_KEY)
13 |
14 | if (store && (await store.get(tag))) {
15 | return
16 | } else {
17 | await updater()
18 | await store.set(tag, true)
19 | }
20 | }
21 |
22 | export async function update() {
23 | updaters.forEach(excute)
24 | }
25 |
26 | export enum PRESET_NAMES {
27 | OPENAI_PRESET = "OpenAI[Preset]",
28 | GOOGLE_PRESET = "Google[Preset]",
29 | OPENROUTER_PRESET = "OpenRouter[Preset]",
30 | DEEPSEEK_PRESET = "DeepSeek[Preset]",
31 | JINA_PRESET = "Jina[Preset]",
32 | SEARXNG_PRESET = "Searxng[Preset]",
33 | }
34 |
35 | export const TAIGA_GITHUB_URL = "https://github.com/Ayuilos/Taiga"
36 |
37 | export const updaters: TUpdater[] = [
38 | [
39 | "0.1.0-rc.0",
40 | async () => {
41 | const presetProviders: IAIProvider[] = [
42 | {
43 | name: PRESET_NAMES.OPENAI_PRESET,
44 | baseURL: "https://api.openai.com/v1",
45 | apiKey: "",
46 | models: [],
47 | preset: true,
48 | },
49 | {
50 | name: PRESET_NAMES.GOOGLE_PRESET,
51 | baseURL: "https://generativelanguage.googleapis.com/v1beta",
52 | apiKey: "",
53 | models: [],
54 | preset: true,
55 | },
56 | {
57 | name: PRESET_NAMES.OPENROUTER_PRESET,
58 | baseURL: "https://openrouter.ai/api/v1",
59 | apiKey: "",
60 | models: [],
61 | preset: true,
62 | },
63 | {
64 | name: PRESET_NAMES.DEEPSEEK_PRESET,
65 | baseURL: "https://api.deepseek.com/v1",
66 | apiKey: "",
67 | models: [],
68 | preset: true,
69 | },
70 | ]
71 |
72 | for (let p of presetProviders) {
73 | await AIProviderManager.addProvider(p)
74 | }
75 |
76 | const presetSearchApis: TBaseSearchAPI[] = [
77 | {
78 | name: PRESET_NAMES.JINA_PRESET,
79 | searchURL: "https://s.jina.ai",
80 | apiKey: "",
81 | },
82 | // [NOTE] Not applicable as public searxng instance always return 429
83 | // haven't find ways to avoid searxng limiter currently
84 | // {
85 | // name: PRESET_NAMES.SEARXNG_PRESET,
86 | // searchURL: "https://baresearch.org",
87 | // },
88 | ]
89 |
90 | for (let p of presetSearchApis) {
91 | await SearchApiStore.setSearchApi(p)
92 | }
93 | },
94 | ],
95 | ["0.1.0-rc.1", async () => {}],
96 | ["0.1.0-rc.2", async () => {}],
97 | ["0.1.0-rc.3", async () => {}],
98 | ["0.1.0-rc.4", async () => {
99 | localStorage.setItem(FONT_SIZE_STORAGE_KEY, FontSize.sm)
100 | localStorage.setItem(FONT_WEIGHT_STORAGE_KEY, FontWeight.normal)
101 | }],
102 | ["0.1.0-rc.5", async () => {}],
103 | ["0.1.0-rc.6", async () => {}],
104 | ["0.1.0-rc.7", async () => {}],
105 | ["0.1.0-rc.8", async () => {}],
106 | ["0.1.0-rc.9", async () => {}],
107 | ["0.1.0-rc.10", async () => {}],
108 | ]
109 | export const getCurrentVersion = () => updaters[updaters.length - 1][0]
110 |
111 | export const checkForUpdates = async (error_fallback: (e: any) => void) => {
112 | try {
113 | const response = await fetch(
114 | "https://api.github.com/repos/Ayuilos/Taiga/releases/latest",
115 | {
116 | headers: {
117 | Accept: "application/vnd.github+json",
118 | "X-GitHub-Api-Version": "2022-11-28",
119 | },
120 | signal: AbortSignal.timeout(10_000),
121 | }
122 | )
123 |
124 | if (response.ok) {
125 | const { tag_name }: GithubAPI.Release = await response.json()
126 | const tagName = tag_name.replace("v", "")
127 |
128 | const store = await load(VERSION_TAGS_STORE_KEY)
129 | const isLatest = await store.has(tagName)
130 |
131 | return { isLatest, tagName }
132 | }
133 | } catch (e) {
134 | error_fallback(e)
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ---
6 |
7 | # 🐯 Taiga
8 |
9 | [](LICENSE)
10 |
11 | ## Introduction
12 |
13 | Taiga is an open-source mobile AI chat app that supports customizing LLM providers.
14 |
15 | > It is a work in progress project, any problem is possible, please use this premise, the author is not responsible for any form of your loss.
16 |
17 | ## Features
18 | - **Customizable LLMs**: Customize the LLM provider you wanna use.
19 | - **Customizable UI**: Customize the UI with your own design (Thanks tweakcn's job).
20 | - **Cross-platform**: Build truly cross-platform mobile apps with Tauri, supporting Android, iOS(later).
21 | - **High Performance**: Tauri uses Rust as the backend, providing higher performance and smaller package sizes than Electron.
22 | - **Security**: Tauri applications have higher security by default.
23 | - **Internationalization**: Support multiple languages with Lingui.
24 |
25 | ### Special note about translation
26 |
27 | All translations are from LLM. Any questions relating to translation are not intended by author, and you are welcome to provide an correction in the [issue](https://github.com/Ayuilos/taiga/issues).
28 |
29 | ## Screenshots
30 |
31 |
32 |
33 |
34 |
35 |
36 | https://github.com/user-attachments/assets/6cc7b182-69b9-4493-aea4-bb73e82cbee7
37 |
38 |
39 | ### 🌟🌟 Download to explore more ⬇
40 |
41 | ## Download
42 |
43 | Go https://github.com/Ayuilos/taiga/releases to find the latest release.
44 |
45 | > Only Android version is available now. iOS is TBD.
46 |
47 | ## Tech Stack
48 |
49 | - [Tauri](https://tauri.app/): A framework for building cross-platform desktop applications.
50 | - [React](https://reactjs.org/): A JavaScript library for building user interfaces.
51 | - [Typescript](https://www.typescriptlang.org/): A superset of JavaScript that provides static type checking.
52 | - [Vite](https://vite.dev/): An extremely fast frontend build tool.
53 | - [VS Code](https://code.visualstudio.com/): Recommended IDE, used with Tauri and rust-analyzer plugins.
54 |
55 | ## Quick Start
56 |
57 | ### Prerequisites
58 |
59 | Please check https://v2.tauri.app/start/prerequisites/ first.
60 |
61 | ### Getting Started
62 |
63 | 1. **Clone the project**:
64 |
65 | ```bash
66 | git clone https://github.com/Ayuilos/taiga.git
67 | cd taiga
68 | ```
69 |
70 | 2. **Install dependencies**:
71 |
72 | ```bash
73 | yarn install
74 | ```
75 |
76 | 3. **Start the development server**:
77 |
78 | ```bash
79 | yarn tauri android dev
80 | ```
81 |
82 | 4. **Build the production version**:
83 |
84 | ```bash
85 | yarn tauri android build
86 | ```
87 |
88 | ## i18N
89 |
90 | Taiga use [lingui](https://lingui.dev/) for i18N.
91 |
92 | ### How to add new words
93 |
94 | #### In project code
95 |
96 | ```ts
97 | // Import the `t` macro
98 | import { t } from "@lingui/macro"
99 |
100 | // Use `xxxString` as a suffix for better readability
101 | const titleString = t`title`
102 | ```
103 |
104 | #### in terminal
105 |
106 | ```bash
107 | # lingui will auto extract the words
108 | yarn extract
109 | ```
110 |
111 | #### Find them in `src/locales/xx/messages.po`
112 |
113 | ```po
114 | #: src/xxx/yyy.tsx:ccc
115 | msgid "title"
116 | msgstr ""
117 | ```
118 |
119 | Add translation in `msgstr`
120 |
121 | ### !!! Be Careful
122 |
123 | Don't run `yarn compile` after `yarn extract`. Project code use `dynamicActivate`
124 |
125 | ## Recommended IDE Setup
126 |
127 | - [VS Code](https://code.visualstudio.com/)
128 | - [Tauri VS Code plugin](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
129 | - [rust-analyzer VS Code plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
130 | - [prettier VS Code plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
131 | - [eslint VS Code plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
132 |
133 | ## License
134 |
135 | This project uses the AGPL-v3 license. For details, please refer to the [LICENSE](LICENSE) file.
136 |
137 | ## Contact
138 |
139 | Leave your problem in https://github.com/Ayuilos/taiga/issues
140 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | useFormState,
12 | } from "react-hook-form"
13 |
14 | import { cn } from "@/lib/utils"
15 | import { Label } from "@/components/ui/label"
16 |
17 | const Form = FormProvider
18 |
19 | type FormFieldContextValue<
20 | TFieldValues extends FieldValues = FieldValues,
21 | TName extends FieldPath = FieldPath,
22 | > = {
23 | name: TName
24 | }
25 |
26 | const FormFieldContext = React.createContext(
27 | {} as FormFieldContextValue
28 | )
29 |
30 | const FormField = <
31 | TFieldValues extends FieldValues = FieldValues,
32 | TName extends FieldPath = FieldPath,
33 | >({
34 | ...props
35 | }: ControllerProps) => {
36 | return (
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | const useFormField = () => {
44 | const fieldContext = React.useContext(FormFieldContext)
45 | const itemContext = React.useContext(FormItemContext)
46 | const { getFieldState } = useFormContext()
47 | const formState = useFormState({ name: fieldContext.name })
48 | const fieldState = getFieldState(fieldContext.name, formState)
49 |
50 | if (!fieldContext) {
51 | throw new Error("useFormField should be used within ")
52 | }
53 |
54 | const { id } = itemContext
55 |
56 | return {
57 | id,
58 | name: fieldContext.name,
59 | formItemId: `${id}-form-item`,
60 | formDescriptionId: `${id}-form-item-description`,
61 | formMessageId: `${id}-form-item-message`,
62 | ...fieldState,
63 | }
64 | }
65 |
66 | type FormItemContextValue = {
67 | id: string
68 | }
69 |
70 | const FormItemContext = React.createContext(
71 | {} as FormItemContextValue
72 | )
73 |
74 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
75 | const id = React.useId()
76 |
77 | return (
78 |
79 |
84 |
85 | )
86 | }
87 |
88 | function FormLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | const { error, formItemId } = useFormField()
93 |
94 | return (
95 |
102 | )
103 | }
104 |
105 | function FormControl({ ...props }: React.ComponentProps) {
106 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
107 |
108 | return (
109 |
120 | )
121 | }
122 |
123 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
124 | const { formDescriptionId } = useFormField()
125 |
126 | return (
127 |
133 | )
134 | }
135 |
136 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
137 | const { error, formMessageId } = useFormField()
138 | const body = error ? String(error?.message) : props.children
139 |
140 | if (!body) {
141 | return null
142 | }
143 |
144 | return (
145 |
151 | {body}
152 |
153 | )
154 | }
155 |
156 | export {
157 | useFormField,
158 | Form,
159 | FormItem,
160 | FormLabel,
161 | FormControl,
162 | FormDescription,
163 | FormMessage,
164 | FormField,
165 | }
166 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { XIcon } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Dialog({
8 | ...props
9 | }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function DialogTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return
17 | }
18 |
19 | function DialogPortal({
20 | ...props
21 | }: React.ComponentProps) {
22 | return
23 | }
24 |
25 | function DialogClose({
26 | ...props
27 | }: React.ComponentProps) {
28 | return
29 | }
30 |
31 | function DialogOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | )
45 | }
46 |
47 | function DialogContent({
48 | className,
49 | children,
50 | showClose = true,
51 | ...props
52 | }: React.ComponentProps & {
53 | showClose?: boolean
54 | }) {
55 | return (
56 |
57 |
58 |
66 | {children}
67 | {showClose ? (
68 |
69 |
70 | Close
71 |
72 | ) : null}
73 |
74 |
75 | )
76 | }
77 |
78 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
79 | return (
80 |
85 | )
86 | }
87 |
88 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
89 | return (
90 |
98 | )
99 | }
100 |
101 | function DialogTitle({
102 | className,
103 | ...props
104 | }: React.ComponentProps) {
105 | return (
106 |
114 | )
115 | }
116 |
117 | function DialogDescription({
118 | className,
119 | ...props
120 | }: React.ComponentProps) {
121 | return (
122 |
127 | )
128 | }
129 |
130 | export {
131 | Dialog,
132 | DialogClose,
133 | DialogContent,
134 | DialogDescription,
135 | DialogFooter,
136 | DialogHeader,
137 | DialogOverlay,
138 | DialogPortal,
139 | DialogTitle,
140 | DialogTrigger,
141 | }
142 |
--------------------------------------------------------------------------------
/src/features/search-api-management.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from "react"
2 | import { zodResolver } from "@hookform/resolvers/zod"
3 | import { Check, X } from "lucide-react"
4 | import { useForm } from "react-hook-form"
5 |
6 | import { SearchApiSchema, TBaseSearchAPI } from "@/lib/search-api-store"
7 | import { NavigationDescription } from "@/components/NavigationDescription"
8 | import { SearchApisContext } from "@/components/SearchApisContext"
9 | import { Button } from "@/components/ui/button"
10 | import {
11 | Form,
12 | FormControl,
13 | FormField,
14 | FormItem,
15 | FormLabel,
16 | FormMessage,
17 | } from "@/components/ui/form"
18 | import { Input } from "@/components/ui/input"
19 | import { PRESET_NAMES } from "@/lib/updater"
20 |
21 | interface IInternalSearchAPIManagement {
22 | apis: TBaseSearchAPI[]
23 | changeApi: (api: TBaseSearchAPI) => Promise
24 | }
25 |
26 | function InternalSearchAPIManagement({
27 | apis,
28 | changeApi,
29 | }: IInternalSearchAPIManagement) {
30 | return (
31 |
32 | {apis.map((api) => {
33 | return (
34 |
39 | )
40 | })}
41 |
42 | )
43 | }
44 |
45 | interface ISingleSearchAPIManagement {
46 | api: TBaseSearchAPI
47 | onAPIChange: (api: TBaseSearchAPI) => void
48 | }
49 | function SingleSearchAPIManagement({
50 | api,
51 | onAPIChange,
52 | }: ISingleSearchAPIManagement) {
53 | const form = useForm({
54 | resolver: zodResolver(SearchApiSchema),
55 | defaultValues: api,
56 | })
57 |
58 | const onSubmit = async (values: TBaseSearchAPI) => {
59 | await onAPIChange(values)
60 |
61 | form.reset(values)
62 | }
63 |
64 | return (
65 |
135 |
136 | )
137 | }
138 |
139 | export function SearchAPIManagement() {
140 | const { searchApis, fetchSearchApis, setSearchApi } =
141 | useContext(SearchApisContext)
142 |
143 | useEffect(() => {
144 | fetchSearchApis()
145 | }, [fetchSearchApis])
146 |
147 | return (
148 |
149 | )
150 | }
151 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 | import { VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { buttonVariants } from "@/components/ui/button"
7 |
8 | function AlertDialog({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function AlertDialogTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return (
18 |
19 | )
20 | }
21 |
22 | function AlertDialogPortal({
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
27 | )
28 | }
29 |
30 | function AlertDialogOverlay({
31 | className,
32 | ...props
33 | }: React.ComponentProps) {
34 | return (
35 |
43 | )
44 | }
45 |
46 | function AlertDialogContent({
47 | className,
48 | ...props
49 | }: React.ComponentProps) {
50 | return (
51 |
52 |
53 |
61 |
62 | )
63 | }
64 |
65 | function AlertDialogHeader({
66 | className,
67 | ...props
68 | }: React.ComponentProps<"div">) {
69 | return (
70 |
75 | )
76 | }
77 |
78 | function AlertDialogFooter({
79 | className,
80 | ...props
81 | }: React.ComponentProps<"div">) {
82 | return (
83 |
91 | )
92 | }
93 |
94 | function AlertDialogTitle({
95 | className,
96 | ...props
97 | }: React.ComponentProps) {
98 | return (
99 |
104 | )
105 | }
106 |
107 | function AlertDialogDescription({
108 | className,
109 | ...props
110 | }: React.ComponentProps) {
111 | return (
112 |
117 | )
118 | }
119 |
120 | function AlertDialogAction({
121 | className,
122 | variant,
123 | ...props
124 | }: React.ComponentProps &
125 | VariantProps) {
126 | return (
127 |
131 | )
132 | }
133 |
134 | function AlertDialogCancel({
135 | className,
136 | ...props
137 | }: React.ComponentProps) {
138 | return (
139 |
143 | )
144 | }
145 |
146 | export {
147 | AlertDialog,
148 | AlertDialogPortal,
149 | AlertDialogOverlay,
150 | AlertDialogTrigger,
151 | AlertDialogContent,
152 | AlertDialogHeader,
153 | AlertDialogFooter,
154 | AlertDialogTitle,
155 | AlertDialogDescription,
156 | AlertDialogAction,
157 | AlertDialogCancel,
158 | }
159 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Command as CommandPrimitive } from "cmdk"
5 | import { SearchIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 | import {
9 | Dialog,
10 | DialogContent,
11 | DialogDescription,
12 | DialogHeader,
13 | DialogTitle,
14 | } from "@/components/ui/dialog"
15 |
16 | function Command({
17 | className,
18 | ...props
19 | }: React.ComponentProps) {
20 | return (
21 |
29 | )
30 | }
31 |
32 | function CommandDialog({
33 | title = "Command Palette",
34 | description = "Search for a command to run...",
35 | children,
36 | ...props
37 | }: React.ComponentProps & {
38 | title?: string
39 | description?: string
40 | }) {
41 | return (
42 |
43 |
44 | {title}
45 | {description}
46 |
47 |
48 |
49 | {children}
50 |
51 |
52 |
53 | )
54 | }
55 |
56 | function CommandInput({
57 | className,
58 | ...props
59 | }: React.ComponentProps) {
60 | return (
61 |
65 |
66 |
74 |
75 | )
76 | }
77 |
78 | function CommandList({
79 | className,
80 | ...props
81 | }: React.ComponentProps) {
82 | return (
83 |
91 | )
92 | }
93 |
94 | function CommandEmpty({
95 | ...props
96 | }: React.ComponentProps) {
97 | return (
98 |
103 | )
104 | }
105 |
106 | function CommandGroup({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
119 | )
120 | }
121 |
122 | function CommandSeparator({
123 | className,
124 | ...props
125 | }: React.ComponentProps) {
126 | return (
127 |
132 | )
133 | }
134 |
135 | function CommandItem({
136 | className,
137 | ...props
138 | }: React.ComponentProps) {
139 | return (
140 |
148 | )
149 | }
150 |
151 | function CommandShortcut({
152 | className,
153 | ...props
154 | }: React.ComponentProps<"span">) {
155 | return (
156 |
164 | )
165 | }
166 |
167 | export {
168 | Command,
169 | CommandDialog,
170 | CommandInput,
171 | CommandList,
172 | CommandEmpty,
173 | CommandGroup,
174 | CommandItem,
175 | CommandShortcut,
176 | CommandSeparator,
177 | }
178 |
--------------------------------------------------------------------------------
/src/features/default-model-management.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useMemo, useState } from "react"
2 | import { t } from "@lingui/core/macro"
3 | import { produce } from "immer"
4 | import {
5 | Check,
6 | ChevronDown,
7 | Languages,
8 | MessageSquare,
9 | Sticker,
10 | } from "lucide-react"
11 |
12 | import { IAIProvider } from "@/lib/ai-providers"
13 | import { TDefaultModel, TDefaultModels } from "@/lib/default-model-store"
14 | import { AIProviderContext } from "@/components/AIProvidersContext"
15 | import { Button } from "@/components/ui/button"
16 | import {
17 | Command,
18 | CommandEmpty,
19 | CommandGroup,
20 | CommandInput,
21 | CommandItem,
22 | CommandList,
23 | } from "@/components/ui/command"
24 | import { Label } from "@/components/ui/label"
25 | import {
26 | Popover,
27 | PopoverContent,
28 | PopoverTrigger,
29 | } from "@/components/ui/popover"
30 |
31 | export function DefaultModelManagement() {
32 | const { providers, defaultModels, fetchDefaultModels, setDefaultModel } =
33 | useContext(AIProviderContext)
34 |
35 | return (
36 |
42 | )
43 | }
44 |
45 | interface IDefaultModelManagement {
46 | providers: IAIProvider[]
47 | defaultModels: TDefaultModels
48 | setDefaultModel: (
49 | type: keyof TDefaultModels,
50 | model: TDefaultModel
51 | ) => Promise
52 | fetchDefaultModels: () => Promise
53 | }
54 |
55 | function InternalDefaultModelManagement({
56 | providers,
57 | defaultModels,
58 | setDefaultModel,
59 | fetchDefaultModels,
60 | }: IDefaultModelManagement) {
61 | useEffect(() => {
62 | fetchDefaultModels()
63 | }, [fetchDefaultModels])
64 |
65 | const labels = {
66 | translate: [ , t`Default Translate Model`],
67 | chat: [ , t`Default Chat Model`],
68 | summarize: [ , t`Default Chat Summarize Model`],
69 | }
70 |
71 | return (
72 |
73 | {Object.entries(defaultModels).map((entry) => {
74 | const type = entry[0] as keyof TDefaultModels
75 | const model = entry[1]
76 |
77 | return (
78 |
79 | {labels[type]}
80 | setDefaultModel(type, model)}
84 | />
85 |
86 | )
87 | })}
88 |
89 | )
90 | }
91 |
92 | interface IDefaultModelSelector {
93 | providers: IAIProvider[]
94 | selectedModel: TDefaultModel
95 | setSelectedModel: (defaultModel: TDefaultModel) => void
96 | }
97 | function DefaultModelSelector({
98 | providers,
99 | selectedModel,
100 | setSelectedModel,
101 | }: IDefaultModelSelector) {
102 | const [open, setOpen] = useState(false)
103 |
104 | const commandList = useMemo(() => {
105 | const sortedProviders = selectedModel
106 | ? produce(providers, (draft) => {
107 | const targetModel = selectedModel ? selectedModel[1] : ""
108 | const index = draft.findIndex((provider) =>
109 | provider.models.includes(targetModel)
110 | )
111 |
112 | if (index !== -1) {
113 | const [provider] = draft.splice(index, 1)
114 | const indexOfModel = provider.models.indexOf(targetModel)
115 | if (indexOfModel !== -1) {
116 | const [model] = provider.models.splice(indexOfModel, 1)
117 | provider.models.unshift(model)
118 | }
119 |
120 | draft.unshift(provider)
121 | }
122 | })
123 | : providers
124 |
125 | return sortedProviders.map((provider) => {
126 | return (
127 |
128 | {provider.models.map((model) => {
129 | const selected = selectedModel && selectedModel[1] === model
130 |
131 | return (
132 | {
136 | setOpen(false)
137 | setSelectedModel([provider.name, model])
138 | }}
139 | >
140 | {model}
141 | {selected && }
142 |
143 | )
144 | })}
145 |
146 | )
147 | })
148 | }, [providers, selectedModel, setSelectedModel])
149 |
150 | const searchModelString = t`Search models...`
151 | const noModelEmptyString = t`No models found`
152 |
153 | return (
154 |
155 |
156 |
157 |
161 |
162 | {selectedModel?.[1]}
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | {noModelEmptyString}
171 | {commandList}
172 |
173 |
174 |
175 |
176 | )
177 | }
178 |
--------------------------------------------------------------------------------
/src/components/ChatHistory.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useState } from "react"
2 | import { t } from "@lingui/core/macro"
3 | import { AlertDialogTrigger } from "@radix-ui/react-alert-dialog"
4 | import { produce } from "immer"
5 |
6 | import {
7 | ChatStore,
8 | ChatSummaryStore,
9 | TChatID,
10 | TChatSummary,
11 | } from "@/lib/chat-store"
12 | import { getRelativeTime } from "@/lib/relative-time"
13 | import {
14 | AlertDialog,
15 | AlertDialogAction,
16 | AlertDialogCancel,
17 | AlertDialogContent,
18 | AlertDialogFooter,
19 | AlertDialogHeader,
20 | } from "./ui/alert-dialog"
21 | import { Badge } from "./ui/badge"
22 | import {
23 | Command,
24 | CommandEmpty,
25 | CommandGroup,
26 | CommandItem,
27 | CommandList,
28 | } from "./ui/command"
29 | import {
30 | Drawer,
31 | DrawerContent,
32 | DrawerDescription,
33 | DrawerHeader,
34 | DrawerTitle,
35 | } from "./ui/drawer"
36 |
37 | interface IChatHistory {
38 | open: boolean
39 | selectedChatId: TChatID | null
40 | onSelect: (id: TChatID) => void
41 | onOpen: (_open: boolean) => void
42 | onHistoryDelete: () => Promise
43 | }
44 | export function ChatHistory({
45 | selectedChatId,
46 | open,
47 | onOpen,
48 | onSelect,
49 | onHistoryDelete,
50 | }: IChatHistory) {
51 | const [chatSummaries, setChatSummaries] = useState([])
52 |
53 | const deleteAllChatHistory = async () => {
54 | await ChatStore.clearAllChats()
55 | setChatSummaries([])
56 | await onHistoryDelete()
57 | }
58 |
59 | const getHistory = useCallback(async () => {
60 | const _chatSummaries = await ChatSummaryStore.getAllSummaries()
61 |
62 | setChatSummaries(_chatSummaries)
63 | }, [])
64 |
65 | useEffect(() => {
66 | if (open) getHistory()
67 | }, [open, getHistory])
68 |
69 | const titleString = t`Chat History`
70 | const deleteAllHistoryString = t`Delete all chat history`
71 | const deleteAllHistoryTitleString = t`Are you sure you want to delete all chat history?`
72 | const deleteAllHistoryConfirmString = t`Confirm`
73 | const deleteAllHistoryCancelString = t`Cancel`
74 | const emptyString = t`No chats found`
75 |
76 | const deleteHistoryBadge = (
77 |
78 |
79 | {deleteAllHistoryString}
80 |
81 |
82 | {deleteAllHistoryTitleString}
83 |
84 |
88 | {deleteAllHistoryConfirmString}
89 |
90 | {deleteAllHistoryCancelString}
91 |
92 |
93 |
94 | )
95 |
96 | const summaryItems = useMemo(() => {
97 | const sortedSummaries = produce(chatSummaries, (draft) =>
98 | draft.sort((a, b) => b.modifiedAt - a.modifiedAt)
99 | )
100 |
101 | const categoriedSummaries = sortedSummaries.reduce<
102 | [string, TChatSummary[]][]
103 | >(
104 | produce((summaryCategores, currSummary) => {
105 | const category = summaryCategores[summaryCategores.length - 1]
106 |
107 | const currSummaryModifiedRelativeTime = getRelativeTime(
108 | new Date(currSummary.modifiedAt)
109 | )
110 | if (category && category[0] === currSummaryModifiedRelativeTime) {
111 | category[1].push(currSummary)
112 | } else {
113 | summaryCategores.push([
114 | currSummaryModifiedRelativeTime,
115 | [currSummary],
116 | ])
117 | }
118 | }),
119 | []
120 | )
121 |
122 | return categoriedSummaries.map(([category, summaries]) => {
123 | return (
124 |
125 | {summaries.map((summary) => {
126 | return (
127 |
132 | {summary.summary}
133 |
134 | )
135 | })}
136 |
137 | )
138 | })
139 | }, [chatSummaries])
140 |
141 | const onValueChange = useCallback(
142 | (value: string) => {
143 | onSelect(value as TChatID)
144 | onOpen(false)
145 | },
146 | [onSelect, onOpen]
147 | )
148 |
149 | return (
150 |
151 |
152 |
153 |
154 | {titleString}
155 | {chatSummaries.length > 0 && deleteHistoryBadge}
156 |
157 |
158 |
159 |
160 | {summaryItems.length > 0 ? (
161 |
165 |
166 | {emptyString}
167 | {summaryItems}
168 |
169 |
170 | ) : (
171 |
172 | {emptyString}
173 |
174 | )}
175 |
176 |
177 |
178 | )
179 | }
180 |
--------------------------------------------------------------------------------
/src/components/ModelSelectorContext.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | useCallback,
4 | useContext,
5 | useMemo,
6 | useState,
7 | } from "react"
8 | import { t } from "@lingui/core/macro"
9 | import { Link } from "@tanstack/react-router"
10 | import { produce } from "immer"
11 | import { Bot, Check, Cloud } from "lucide-react"
12 |
13 | import { AIProviderContext, TAIProviderContextType } from "./AIProvidersContext"
14 | import { Badge } from "./ui/badge"
15 | import { Button } from "./ui/button"
16 | import {
17 | CommandDialog,
18 | CommandEmpty,
19 | CommandGroup,
20 | CommandInput,
21 | CommandItem,
22 | CommandList,
23 | } from "./ui/command"
24 |
25 | export type TModelSelectorContext = ReturnType
26 | export const ModelSelectorContext = createContext({
27 | showModel: false,
28 | closeModel: () => {},
29 | requireModel: () => {},
30 | })
31 | export function ModelSelectorContextProvider({
32 | children,
33 | }: {
34 | children: React.ReactNode
35 | }) {
36 | const context = useModelSelector()
37 |
38 | return (
39 |
40 | {children}
41 |
42 | )
43 | }
44 |
45 | function useModelSelector() {
46 | const { fetchProviders } = useContext(AIProviderContext)
47 |
48 | const [showModel, setShowModel] = useState(false)
49 |
50 | const requireModel = useCallback(
51 | (callback?: () => void) => {
52 | if (!showModel) {
53 | setShowModel(true)
54 | fetchProviders().then(callback)
55 | }
56 | },
57 | [fetchProviders, showModel]
58 | )
59 |
60 | const closeModel = useCallback(() => {
61 | setShowModel(false)
62 | }, [])
63 |
64 | return {
65 | showModel,
66 | closeModel,
67 | requireModel,
68 | }
69 | }
70 |
71 | function InternalModelSelector({
72 | providers,
73 | selectedModel,
74 | showModel,
75 | setSelectedModel,
76 | requireModel,
77 | closeModel,
78 | }: TAIProviderContextType & TModelSelectorContext) {
79 | // Check if `selectedModel` is still in `provider.models` and update if not
80 | // No unlimited rerender risk found
81 | if (selectedModel) {
82 | if (
83 | !providers.some((provider) => provider.models.includes(selectedModel[1]))
84 | ) {
85 | setSelectedModel(null)
86 | }
87 | }
88 |
89 | const commandList = useMemo(() => {
90 | const sortedProviders = selectedModel
91 | ? produce(providers, (draft) => {
92 | const targetModel = selectedModel ? selectedModel[1] : ""
93 | const index = draft.findIndex((provider) =>
94 | provider.models.includes(targetModel)
95 | )
96 |
97 | if (index !== -1) {
98 | const [provider] = draft.splice(index, 1)
99 | const indexOfModel = provider.models.indexOf(targetModel)
100 | if (indexOfModel !== -1) {
101 | const [model] = provider.models.splice(indexOfModel, 1)
102 | provider.models.unshift(model)
103 | }
104 |
105 | draft.unshift(provider)
106 | }
107 | })
108 | : providers
109 |
110 | return sortedProviders.map((provider) => {
111 | return (
112 | {`- ${provider.name} -`}}
115 | >
116 | {provider.models.map((model) => {
117 | const selected = selectedModel && selectedModel[1] === model
118 |
119 | return (
120 | {
124 | closeModel()
125 | setSelectedModel([provider.name, model])
126 | }}
127 | >
128 | {model}
129 | {selected && }
130 |
131 | )
132 | })}
133 |
134 | )
135 | })
136 | }, [providers, selectedModel, setSelectedModel, closeModel])
137 |
138 | const selectModelString = t`Select a model`
139 | const selectorName = selectedModel ? selectedModel[1] : selectModelString
140 | const providerNameOfSelectedModel = selectedModel?.[0]
141 | const emptyString = t`No models found`
142 | const goProviderConfigLinkString = t`Add models`
143 |
144 | return (
145 | <>
146 | requireModel()}
151 | >
152 |
153 | {selectorName}
154 | {providerNameOfSelectedModel ? (
155 |
156 |
157 | {providerNameOfSelectedModel}
158 |
159 | ) : null}
160 |
161 |
162 |
163 |
164 |
165 | {emptyString}
166 |
167 |
168 | {goProviderConfigLinkString}
169 |
170 |
171 | {commandList}
172 |
173 |
174 | >
175 | )
176 | }
177 |
178 | export default function ModelSelector() {
179 | const providerContext = useContext(AIProviderContext)
180 | const selectorContext = useContext(ModelSelectorContext)
181 |
182 | return
183 | }
184 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/src-tauri/gen/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | @plugin "tailwindcss-animate";
4 |
5 | @custom-variant dark (&:is(.dark *));
6 |
7 | :root {
8 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
9 | font-size: 16px;
10 | line-height: 1.5em;
11 | font-weight: 400;
12 |
13 | color: #0f0f0f;
14 | background-color: #f6f6f6;
15 |
16 | font-synthesis: none;
17 | text-rendering: optimizeLegibility;
18 | -webkit-font-smoothing: antialiased;
19 | -moz-osx-font-smoothing: grayscale;
20 | -webkit-text-size-adjust: 100%;
21 | --background: hsl(0 0% 100%);
22 | --foreground: hsl(0 0% 3.9%);
23 | --card: hsl(0 0% 100%);
24 | --card-foreground: hsl(0 0% 3.9%);
25 | --popover: hsl(0 0% 100%);
26 | --popover-foreground: hsl(0 0% 3.9%);
27 | --primary: hsl(0 0% 9%);
28 | --primary-foreground: hsl(0 0% 98%);
29 | --secondary: hsl(0 0% 96.1%);
30 | --secondary-foreground: hsl(0 0% 9%);
31 | --muted: hsl(0 0% 96.1%);
32 | --muted-foreground: hsl(0 0% 45.1%);
33 | --accent: hsl(0 0% 96.1%);
34 | --accent-foreground: hsl(0 0% 9%);
35 | --destructive: hsl(0 84.2% 60.2%);
36 | --destructive-foreground: hsl(0 0% 98%);
37 | --border: hsl(0 0% 89.8%);
38 | --input: hsl(0 0% 89.8%);
39 | --ring: hsl(0 0% 3.9%);
40 | --chart-1: hsl(12 76% 61%);
41 | --chart-2: hsl(173 58% 39%);
42 | --chart-3: hsl(197 37% 24%);
43 | --chart-4: hsl(43 74% 66%);
44 | --chart-5: hsl(27 87% 67%);
45 | --radius: 0.6rem;
46 | }
47 |
48 | .dark {
49 | --background: hsl(0 0% 3.9%);
50 | --foreground: hsl(0 0% 98%);
51 | --card: hsl(0 0% 3.9%);
52 | --card-foreground: hsl(0 0% 98%);
53 | --popover: hsl(0 0% 3.9%);
54 | --popover-foreground: hsl(0 0% 98%);
55 | --primary: hsl(0 0% 98%);
56 | --primary-foreground: hsl(0 0% 9%);
57 | --secondary: hsl(0 0% 14.9%);
58 | --secondary-foreground: hsl(0 0% 98%);
59 | --muted: hsl(0 0% 14.9%);
60 | --muted-foreground: hsl(0 0% 63.9%);
61 | --accent: hsl(0 0% 14.9%);
62 | --accent-foreground: hsl(0 0% 98%);
63 | --destructive: hsl(0 62.8% 30.6%);
64 | --destructive-foreground: hsl(0 0% 98%);
65 | --border: hsl(0 0% 14.9%);
66 | --input: hsl(0 0% 14.9%);
67 | --ring: hsl(0 0% 83.1%);
68 | --chart-1: hsl(220 70% 50%);
69 | --chart-2: hsl(160 60% 45%);
70 | --chart-3: hsl(30 80% 55%);
71 | --chart-4: hsl(280 65% 60%);
72 | --chart-5: hsl(340 75% 55%);
73 | }
74 |
75 | .taiga-theme {
76 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
77 | font-size: 16px;
78 | line-height: 24px;
79 | font-weight: 400;
80 |
81 | color: #0f0f0f;
82 | background-color: #f6f6f6;
83 |
84 | font-synthesis: none;
85 | text-rendering: optimizeLegibility;
86 | -webkit-font-smoothing: antialiased;
87 | -moz-osx-font-smoothing: grayscale;
88 | -webkit-text-size-adjust: 100%;
89 | --background: hsl(35 55 98%);
90 | --foreground: hsl(35 55 50%);
91 | --card: hsl(35 55 96%);
92 | --card-foreground: hsl(35 55 50%);
93 | --popover: hsl(35 55 96%);
94 | --popover-foreground: hsl(35 55 64%);
95 | --primary: hsl(35 55 50%);
96 | --primary-foreground: hsl(35 55 98%);
97 | --secondary: hsl(35 55 68%);
98 | --secondary-foreground: hsl(35 55 98%);
99 | --muted: hsl(35 55 88%);
100 | --muted-foreground: hsl(35 55 64%);
101 | --accent: hsl(35 55 88%);
102 | --accent-foreground: hsl(35 55 50%);
103 | --destructive: hsl(0 84.2% 60.2%);
104 | --destructive-foreground: hsl(0 0% 98%);
105 | --border: hsl(35 55 80%);
106 | --input: hsl(35 55 80%);
107 | --ring: hsl(35 55 56%);
108 | --chart-1: hsl(12 76% 61%);
109 | --chart-2: hsl(173 58% 39%);
110 | --chart-3: hsl(197 37% 24%);
111 | --chart-4: hsl(43 74% 66%);
112 | --chart-5: hsl(27 87% 67%);
113 | --radius: 0.6rem;
114 | }
115 |
116 | @theme inline {
117 | --color-background: var(--background);
118 | --color-foreground: var(--foreground);
119 | --color-card: var(--card);
120 | --color-card-foreground: var(--card-foreground);
121 | --color-popover: var(--popover);
122 | --color-popover-foreground: var(--popover-foreground);
123 | --color-primary: var(--primary);
124 | --color-primary-foreground: var(--primary-foreground);
125 | --color-secondary: var(--secondary);
126 | --color-secondary-foreground: var(--secondary-foreground);
127 | --color-muted: var(--muted);
128 | --color-muted-foreground: var(--muted-foreground);
129 | --color-accent: var(--accent);
130 | --color-accent-foreground: var(--accent-foreground);
131 | --color-destructive: var(--destructive);
132 | --color-destructive-foreground: var(--destructive-foreground);
133 | --color-border: var(--border);
134 | --color-input: var(--input);
135 | --color-ring: var(--ring);
136 | --color-chart-1: var(--chart-1);
137 | --color-chart-2: var(--chart-2);
138 | --color-chart-3: var(--chart-3);
139 | --color-chart-4: var(--chart-4);
140 | --color-chart-5: var(--chart-5);
141 | --radius-sm: calc(var(--radius) - 4px);
142 | --radius-md: calc(var(--radius) - 2px);
143 | --radius-lg: var(--radius);
144 | --radius-xl: calc(var(--radius) + 4px);
145 | }
146 |
147 | @theme {
148 | --animate-loading-gradient: loading-gradient-keyframe 6s infinite linear;
149 | --font-aeche: Aeche, sans-serif;
150 |
151 | @keyframes loading-gradient-keyframe {
152 | 0% {
153 | background-position: 200% 0%;
154 | }
155 | 100% {
156 | background-position: -200% 0%;
157 | }
158 | }
159 | }
160 |
161 | @font-face {
162 | font-family: Aeche;
163 | font-style: normal;
164 | font-weight: 200;
165 | font-display: auto;
166 | src: url("/fonts/Aeche-light.woff2") format("woff2");
167 | }
168 |
169 | @font-face {
170 | font-family: Aeche;
171 | font-style: normal;
172 | font-weight: 400;
173 | font-display: auto;
174 | src: url("/fonts/Aeche-regular.woff2") format("woff2");
175 | }
176 |
177 | @font-face {
178 | font-family: Aeche;
179 | font-style: normal;
180 | font-weight: 600;
181 | font-display: auto;
182 | src: url("/fonts/Aeche-bold.woff2") format("woff2");
183 | }
184 |
185 | @font-face {
186 | font-family: Aeche;
187 | font-style: normal;
188 | font-weight: 800;
189 | font-display: auto;
190 | src: url("/fonts/Aeche-extrabold.woff2") format("woff2");
191 | }
192 |
193 | @font-face {
194 | font-family: Aeche;
195 | font-style: normal;
196 | font-weight: 900;
197 | font-display: auto;
198 | src: url("/fonts/Aeche-black.woff2") format("woff2");
199 | }
200 |
201 | @layer base {
202 | * {
203 | @apply border-border outline-ring/50;
204 | }
205 | body {
206 | @apply bg-background text-foreground;
207 | }
208 | button {
209 | @apply select-none;
210 | }
211 | th,
212 | td {
213 | @apply p-2 border;
214 | }
215 | th {
216 | @apply bg-muted;
217 | }
218 | }
219 |
--------------------------------------------------------------------------------