diff --git a/.github/workflows/template-tauri-build-linux-x64-external.yml b/.github/workflows/template-tauri-build-linux-x64-external.yml index a88c48267..5c39e17c7 100644 --- a/.github/workflows/template-tauri-build-linux-x64-external.yml +++ b/.github/workflows/template-tauri-build-linux-x64-external.yml @@ -70,10 +70,9 @@ jobs: run: | echo "Version: ${{ inputs.new_version }}" jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = false' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json - mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json + mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json if [ "${{ inputs.channel }}" != "stable" ]; then - jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun", - "usr/lib/Jan-${{ inputs.channel }}/resources/lib/libvulkan.so": "resources/lib/libvulkan.so"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json + jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json mv /tmp/tauri.linux.conf.json ./src-tauri/tauri.linux.conf.json fi jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json @@ -83,7 +82,7 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json mv /tmp/package.json ./src-tauri/plugins/tauri-plugin-hardware/package.json - + echo "---------./src-tauri/plugins/tauri-plugin-hardware/package.json---------" cat ./src-tauri/plugins/tauri-plugin-hardware/package.json @@ -96,7 +95,7 @@ jobs: ctoml ./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml---------" cat ./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml - + ctoml ./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml---------" cat ./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml @@ -125,7 +124,7 @@ jobs: env: RELEASE_CHANNEL: '${{ inputs.channel }}' AUTO_UPDATER_DISABLED: ${{ inputs.disable_updater && 'true' || 'false' }} - + - name: Upload Artifact uses: actions/upload-artifact@v4 with: @@ -136,4 +135,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: jan-linux-amd64-${{ inputs.new_version }}-AppImage - path: ./src-tauri/target/release/bundle/appimage/*.AppImage \ No newline at end of file + path: ./src-tauri/target/release/bundle/appimage/*.AppImage diff --git a/.github/workflows/template-tauri-build-linux-x64-flatpak.yml b/.github/workflows/template-tauri-build-linux-x64-flatpak.yml index 15d4827f7..d63fae3e7 100644 --- a/.github/workflows/template-tauri-build-linux-x64-flatpak.yml +++ b/.github/workflows/template-tauri-build-linux-x64-flatpak.yml @@ -91,10 +91,9 @@ jobs: echo "Version: ${{ inputs.new_version }}" # Update tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json - mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json + mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json if [ "${{ inputs.channel }}" != "stable" ]; then - jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun", - "usr/lib/Jan-${{ inputs.channel }}/resources/lib/libvulkan.so": "resources/lib/libvulkan.so"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json + jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json mv /tmp/tauri.linux.conf.json ./src-tauri/tauri.linux.conf.json fi jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json @@ -104,7 +103,7 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json mv /tmp/package.json ./src-tauri/plugins/tauri-plugin-hardware/package.json - + echo "---------./src-tauri/plugins/tauri-plugin-hardware/package.json---------" cat ./src-tauri/plugins/tauri-plugin-hardware/package.json @@ -117,7 +116,7 @@ jobs: ctoml ./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml---------" cat ./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml - + ctoml ./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml---------" cat ./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml @@ -128,7 +127,7 @@ jobs: # Temporarily enable devtool on prod build ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" - cat ./src-tauri/Cargo.toml + cat ./src-tauri/Cargo.toml # Change app name for beta and nightly builds if [ "${{ inputs.channel }}" != "stable" ]; then @@ -139,7 +138,7 @@ jobs: .github/scripts/rename-tauri-app.sh ./src-tauri/tauri.conf.json ${{ inputs.channel }} cat ./src-tauri/tauri.conf.json - + # Update Cargo.toml ctoml ./src-tauri/Cargo.toml package.name "Jan-${{ inputs.channel }}" ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" @@ -184,4 +183,3 @@ jobs: with: name: jan-linux-amd64-flatpak-${{ inputs.new_version }}-AppImage path: ./src-tauri/target/release/bundle/appimage/*.AppImage - diff --git a/.github/workflows/template-tauri-build-linux-x64.yml b/.github/workflows/template-tauri-build-linux-x64.yml index bd9b38369..487571595 100644 --- a/.github/workflows/template-tauri-build-linux-x64.yml +++ b/.github/workflows/template-tauri-build-linux-x64.yml @@ -108,10 +108,9 @@ jobs: echo "Version: ${{ inputs.new_version }}" # Update tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json - mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json + mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json if [ "${{ inputs.channel }}" != "stable" ]; then - jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun", - "usr/lib/Jan-${{ inputs.channel }}/resources/lib/libvulkan.so": "resources/lib/libvulkan.so"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json + jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json mv /tmp/tauri.linux.conf.json ./src-tauri/tauri.linux.conf.json fi jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json @@ -121,7 +120,7 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/plugins/tauri-plugin-hardware/package.json > /tmp/package.json mv /tmp/package.json ./src-tauri/plugins/tauri-plugin-hardware/package.json - + echo "---------./src-tauri/plugins/tauri-plugin-hardware/package.json---------" cat ./src-tauri/plugins/tauri-plugin-hardware/package.json @@ -134,7 +133,7 @@ jobs: ctoml ./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml---------" cat ./src-tauri/plugins/tauri-plugin-hardware/Cargo.toml - + ctoml ./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml---------" cat ./src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml @@ -156,7 +155,7 @@ jobs: .github/scripts/rename-tauri-app.sh ./src-tauri/tauri.conf.json ${{ inputs.channel }} cat ./src-tauri/tauri.conf.json - + # Update Cargo.toml ctoml ./src-tauri/Cargo.toml package.name "Jan-${{ inputs.channel }}" ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" diff --git a/Makefile b/Makefile index 9a03ddaad..e2262a842 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,6 @@ endif dev: install-and-build yarn download:bin - yarn download:lib yarn dev # Web application targets @@ -58,7 +57,7 @@ build-web-app: install-web-app yarn build:core yarn build:web-app -serve-web-app: +serve-web-app: yarn serve:web-app build-serve-web-app: build-web-app @@ -71,7 +70,6 @@ lint: install-and-build # Testing test: lint yarn download:bin - yarn download:lib ifeq ($(OS),Windows_NT) yarn download:windows-installer endif diff --git a/README.md b/README.md index 656917634..b2de4407f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Jan - Local AI Assistant +# Jan - Open-source ChatGPT replacement -![Jan AI](docs/src/pages/docs/_assets/jan-app.png) +github jan banner

@@ -12,15 +12,13 @@

- Getting Started - - Docs + Getting Started + - Community - Changelog - Bug reports - - Discord

-Jan is an AI assistant that can run 100% offline on your device. Download and run LLMs with -**full control** and **privacy**. +Jan is bringing the best of open-source AI in an easy-to-use product. Download and run LLMs with **full control** and **privacy**. ## Installation @@ -29,41 +27,36 @@ The easiest way to get started is by downloading one of the following versions f - - + - - - -
PlatformStableNightlyDownload
Windows jan.exejan.exe
macOS jan.dmgjan.dmg
Linux (deb) jan.debjan.deb
Linux (AppImage) jan.AppImagejan.AppImage
-Download from [jan.ai](https://jan.ai/) or [GitHub Releases](https://github.com/menloresearch/jan/releases). +Download from [jan.ai](https://jan.ai/) or [GitHub Releases](https://github.com/menloresearch/jan/releases). ## Features -- **Local AI Models**: Download and run LLMs (Llama, Gemma, Qwen, etc.) from HuggingFace -- **Cloud Integration**: Connect to OpenAI, Anthropic, Mistral, Groq, and others +- **Local AI Models**: Download and run LLMs (Llama, Gemma, Qwen, GPT-oss etc.) from HuggingFace +- **Cloud Integration**: Connect to GPT models via OpenAI, Claude models via Anthropic, Mistral, Groq, and others - **Custom Assistants**: Create specialized AI assistants for your tasks - **OpenAI-Compatible API**: Local server at `localhost:1337` for other applications -- **Model Context Protocol**: MCP integration for enhanced capabilities +- **Model Context Protocol**: MCP integration for agentic capabilities - **Privacy First**: Everything runs locally when you want it to ## Build from Source diff --git a/core/package.json b/core/package.json index eec56a733..203eaf293 100644 --- a/core/package.json +++ b/core/package.json @@ -27,11 +27,13 @@ "devDependencies": { "@npmcli/arborist": "^7.1.0", "@types/node": "^22.10.0", + "@types/react": "19.1.2", "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", "eslint": "8.57.0", "happy-dom": "^15.11.6", "pacote": "^21.0.0", + "react": "19.0.0", "request": "^2.88.2", "request-progress": "^3.0.0", "rimraf": "^6.0.1", @@ -44,5 +46,8 @@ "rxjs": "^7.8.1", "ulidx": "^2.3.0" }, + "peerDependencies": { + "react": "19.0.0" + }, "packageManager": "yarn@4.5.3" } diff --git a/core/rolldown.config.mjs b/core/rolldown.config.mjs index fd3329ee0..fbb2bd351 100644 --- a/core/rolldown.config.mjs +++ b/core/rolldown.config.mjs @@ -10,7 +10,7 @@ export default defineConfig([ sourcemap: true, }, platform: 'browser', - external: ['path'], + external: ['path', 'react', 'react-dom', 'react/jsx-runtime'], define: { NODE: JSON.stringify(`${pkgJson.name}/${pkgJson.node}`), VERSION: JSON.stringify(pkgJson.version), diff --git a/core/src/browser/extensions/conversational.test.ts b/core/src/browser/extensions/conversational.test.ts index c08468905..44d1e9b4f 100644 --- a/core/src/browser/extensions/conversational.test.ts +++ b/core/src/browser/extensions/conversational.test.ts @@ -250,4 +250,4 @@ describe('ConversationalExtension', () => { expect(retrievedAssistant.modelId).toBe('') }) -}) \ No newline at end of file +}) diff --git a/core/src/browser/extensions/engines/LocalOAIEngine.test.ts b/core/src/browser/extensions/engines/LocalOAIEngine.test.ts index 5f2563d56..3523c3ce6 100644 --- a/core/src/browser/extensions/engines/LocalOAIEngine.test.ts +++ b/core/src/browser/extensions/engines/LocalOAIEngine.test.ts @@ -131,4 +131,4 @@ describe('LocalOAIEngine', () => { expect(engine.loadedModel).toBeUndefined() }) }) -}) \ No newline at end of file +}) diff --git a/core/src/browser/extensions/mcp.test.ts b/core/src/browser/extensions/mcp.test.ts index ece971809..8ba3f200a 100644 --- a/core/src/browser/extensions/mcp.test.ts +++ b/core/src/browser/extensions/mcp.test.ts @@ -96,4 +96,4 @@ describe('MCPExtension', () => { expect(healthy).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/core/src/browser/extensions/mcp.ts b/core/src/browser/extensions/mcp.ts index 7f30a5428..8a188478a 100644 --- a/core/src/browser/extensions/mcp.ts +++ b/core/src/browser/extensions/mcp.ts @@ -1,5 +1,6 @@ -import { MCPInterface, MCPTool, MCPToolCallResult } from '../../types' +import { MCPInterface, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '../../types' import { BaseExtension, ExtensionTypeEnum } from '../extension' +import type { ComponentType } from 'react' /** * MCP (Model Context Protocol) extension for managing tools and server communication. @@ -18,4 +19,10 @@ export abstract class MCPExtension extends BaseExtension implements MCPInterface abstract getConnectedServers(): Promise abstract refreshTools(): Promise abstract isHealthy(): Promise -} \ No newline at end of file + + /** + * Optional method to provide a custom UI component for tools + * @returns A React component or null if no custom component is provided + */ + getToolComponent?(): ComponentType | null +} diff --git a/core/src/browser/models/manager.test.ts b/core/src/browser/models/manager.test.ts index 90626b22e..0e4e728cf 100644 --- a/core/src/browser/models/manager.test.ts +++ b/core/src/browser/models/manager.test.ts @@ -131,4 +131,4 @@ describe('ModelManager', () => { expect(modelManager.models.get('model-2')).toEqual(model2) }) }) -}) \ No newline at end of file +}) diff --git a/core/src/test/setup.ts b/core/src/test/setup.ts index c597a3748..6f3d766bf 100644 --- a/core/src/test/setup.ts +++ b/core/src/test/setup.ts @@ -16,4 +16,4 @@ if (!window.core) { }) } -// Add any other global mocks needed for core tests \ No newline at end of file +// Add any other global mocks needed for core tests diff --git a/core/src/types/mcp/index.ts b/core/src/types/mcp/index.ts index 4ffd501fc..bd809d790 100644 --- a/core/src/types/mcp/index.ts +++ b/core/src/types/mcp/index.ts @@ -1,2 +1,2 @@ export * from './mcpEntity' -export * from './mcpInterface' \ No newline at end of file +export * from './mcpInterface' diff --git a/core/src/types/mcp/mcpEntity.ts b/core/src/types/mcp/mcpEntity.ts index a2259e52e..6c7e0c598 100644 --- a/core/src/types/mcp/mcpEntity.ts +++ b/core/src/types/mcp/mcpEntity.ts @@ -21,4 +21,18 @@ export interface MCPServerInfo { name: string connected: boolean tools?: MCPTool[] -} \ No newline at end of file +} + +/** + * Props for MCP tool UI components + */ +export interface MCPToolComponentProps { + /** List of available MCP tools */ + tools: MCPTool[] + + /** Function to check if a specific tool is currently enabled */ + isToolEnabled: (toolName: string) => boolean + + /** Function to toggle a tool's enabled/disabled state */ + onToolToggle: (toolName: string, enabled: boolean) => void +} diff --git a/core/src/types/mcp/mcpInterface.ts b/core/src/types/mcp/mcpInterface.ts index 15152a83b..a656e9f66 100644 --- a/core/src/types/mcp/mcpInterface.ts +++ b/core/src/types/mcp/mcpInterface.ts @@ -29,4 +29,4 @@ export interface MCPInterface { * Check if MCP service is healthy */ isHealthy(): Promise -} \ No newline at end of file +} diff --git a/docs/_redirects b/docs/_redirects index 17342a020..b72c65400 100644 --- a/docs/_redirects +++ b/docs/_redirects @@ -112,6 +112,12 @@ /docs/remote-models/openrouter /docs/desktop/remote-models/openrouter 302 /docs/server-examples/llmcord /docs/desktop/server-examples/llmcord 302 /docs/server-examples/tabby /docs/desktop/server-examples/tabby 302 +/docs/built-in/tensorrt-llm /docs/desktop/llama-cpp 302 +/docs/desktop/docs/desktop/linux /docs/desktop/install/linux 302 +/windows /docs/desktop/install/windows 302 +/docs/quickstart /docs/ 302 +/docs/desktop/mac /docs/desktop/install/mac 302 +/handbook/open-superintelligence /handbook/why/open-superintelligence 302 /guides/integrations/continue/ /docs/desktop/server-examples/continue-dev 302 /continue-dev /docs/desktop/server-examples/continue-dev 302 @@ -130,4 +136,4 @@ /local-server/troubleshooting /docs/desktop/troubleshooting 302 /mcp /docs/desktop/mcp 302 /quickstart /docs/desktop/quickstart 302 -/server-examples/continue-dev /docs/desktop/server-examples/continue-dev 302 \ No newline at end of file +/server-examples/continue-dev /docs/desktop/server-examples/continue-dev 302 diff --git a/docs/plopfile.js b/docs/plopfile.js index a31caa889..0c384af85 100644 --- a/docs/plopfile.js +++ b/docs/plopfile.js @@ -6,7 +6,7 @@ const camelCase = (str) => { return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()) } -const categories = ['building-jan', 'research'] +const categories = ['building-jan', 'research', 'guides'] /** * @param {import("plop").NodePlopAPI} plop diff --git a/docs/public/assets/images/general/ai-for-teacher.jpeg b/docs/public/assets/images/general/ai-for-teacher.jpeg new file mode 100644 index 000000000..c600389b3 Binary files /dev/null and b/docs/public/assets/images/general/ai-for-teacher.jpeg differ diff --git a/docs/public/assets/images/general/assistants-ai-for-teachers.jpeg b/docs/public/assets/images/general/assistants-ai-for-teachers.jpeg new file mode 100644 index 000000000..b9aedf135 Binary files /dev/null and b/docs/public/assets/images/general/assistants-ai-for-teachers.jpeg differ diff --git a/docs/public/assets/images/general/chatgpt-alternative-jan.jpeg b/docs/public/assets/images/general/chatgpt-alternative-jan.jpeg new file mode 100644 index 000000000..c4e18913a Binary files /dev/null and b/docs/public/assets/images/general/chatgpt-alternative-jan.jpeg differ diff --git a/docs/src/pages/post/_assets/cover-kernel-benchmarking.png b/docs/public/assets/images/general/cover-kernel-benchmarking.png similarity index 100% rename from docs/src/pages/post/_assets/cover-kernel-benchmarking.png rename to docs/public/assets/images/general/cover-kernel-benchmarking.png diff --git a/docs/src/pages/post/_assets/deepseek-r1-locally-jan.jpg b/docs/public/assets/images/general/deepseek-r1-locally-jan.jpg similarity index 100% rename from docs/src/pages/post/_assets/deepseek-r1-locally-jan.jpg rename to docs/public/assets/images/general/deepseek-r1-locally-jan.jpg diff --git a/docs/src/pages/post/_assets/gpt-oss locally.jpeg b/docs/public/assets/images/general/gpt-oss locally.jpeg similarity index 100% rename from docs/src/pages/post/_assets/gpt-oss locally.jpeg rename to docs/public/assets/images/general/gpt-oss locally.jpeg diff --git a/docs/public/assets/images/general/is-chatgpt-down.jpg b/docs/public/assets/images/general/is-chatgpt-down.jpg new file mode 100644 index 000000000..2a515d344 Binary files /dev/null and b/docs/public/assets/images/general/is-chatgpt-down.jpg differ diff --git a/docs/public/assets/images/general/jan-ai-for-teacher.mp4 b/docs/public/assets/images/general/jan-ai-for-teacher.mp4 new file mode 100644 index 000000000..3930e6e26 Binary files /dev/null and b/docs/public/assets/images/general/jan-ai-for-teacher.mp4 differ diff --git a/docs/public/assets/images/general/jan-assistants-ai-for-legal.jpeg b/docs/public/assets/images/general/jan-assistants-ai-for-legal.jpeg new file mode 100644 index 000000000..b49de6380 Binary files /dev/null and b/docs/public/assets/images/general/jan-assistants-ai-for-legal.jpeg differ diff --git a/docs/public/assets/images/general/jan-for-ai-law-assistant-chat.jpeg b/docs/public/assets/images/general/jan-for-ai-law-assistant-chat.jpeg new file mode 100644 index 000000000..5501b5e70 Binary files /dev/null and b/docs/public/assets/images/general/jan-for-ai-law-assistant-chat.jpeg differ diff --git a/docs/src/pages/post/_assets/offline-chatgpt-alternatives-jan.jpg b/docs/public/assets/images/general/offline-chatgpt-alternatives-jan.jpg similarity index 100% rename from docs/src/pages/post/_assets/offline-chatgpt-alternatives-jan.jpg rename to docs/public/assets/images/general/offline-chatgpt-alternatives-jan.jpg diff --git a/docs/src/pages/post/_assets/qwen3-settings-jan-ai.jpeg b/docs/public/assets/images/general/qwen3-30b-settings.jpg similarity index 100% rename from docs/src/pages/post/_assets/qwen3-settings-jan-ai.jpeg rename to docs/public/assets/images/general/qwen3-30b-settings.jpg diff --git a/docs/src/pages/post/_assets/research-result-local.png b/docs/public/assets/images/general/research-result-local.png similarity index 100% rename from docs/src/pages/post/_assets/research-result-local.png rename to docs/public/assets/images/general/research-result-local.png diff --git a/docs/src/pages/post/_assets/run-ai-locally-with-jan.jpg b/docs/public/assets/images/general/run-ai-locally-with-jan.jpg similarity index 100% rename from docs/src/pages/post/_assets/run-ai-locally-with-jan.jpg rename to docs/public/assets/images/general/run-ai-locally-with-jan.jpg diff --git a/docs/public/sitemap-0.xml b/docs/public/sitemap-0.xml deleted file mode 100644 index 131222295..000000000 --- a/docs/public/sitemap-0.xml +++ /dev/null @@ -1,125 +0,0 @@ - - -https://jan.ai2025-09-24T03:40:05.491Zdaily1 -https://jan.ai/api-reference2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/api-reference/api-reference2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/api-reference/architecture2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/api-reference/configuration2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/api-reference/development2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/api-reference/installation2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/blog2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2023-12-21-faster-inference-across-platform2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-01-16-settings-options-right-panel2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-01-29-local-api-server2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-02-05-jan-data-folder2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-02-10-jan-is-more-stable2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-02-26-home-servers-with-helm2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-03-06-ui-revamp-settings2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-03-11-import-models2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-03-19-nitro-tensorrt-llm-extension2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-04-02-groq-api-integration2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-04-15-new-mistral-extension2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-04-25-llama3-command-r-hugginface2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-05-20-llamacpp-upgrade-new-remote-models2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-05-28-cohere-aya-23-8b-35b-phi-3-medium2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-06-21-nvidia-nim-support2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-07-15-claude-3-5-support2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-09-01-llama3-1-gemma2-support2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-09-17-improved-cpu-performance2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-10-24-jan-stable2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-11-22-jan-bugs2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-11.14-jan-supports-qwen-coder2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-12-03-jan-is-faster2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-12-05-jan-hot-fix-mac2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2024-12-30-jan-new-privacy2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-01-06-key-issues-resolved2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-01-23-deepseek-r1-jan2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-02-18-advanced-llama.cpp-settings2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-03-14-jan-security-patch2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-05-14-jan-qwen3-patch2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-06-19-jan-ui-revamp2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-06-26-jan-nano-mcp2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-07-17-responsive-ui2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-07-31-llamacpp-tutorials2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-08-07-gpt-oss2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-08-14-general-improvs2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-08-28-image-support2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/changelog/2025-09-18-auto-optimize-vision-imports2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/api-server2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/assistants2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/data-folder2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/install/linux2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/install/mac2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/install/windows2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/jan-models/jan-nano-1282025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/jan-models/jan-nano-322025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/jan-models/jan-v12025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/jan-models/lucy2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/llama-cpp2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/llama-cpp-server2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/manage-models2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/browser/browserbase2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/data-analysis/e2b2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/data-analysis/jupyter2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/deepresearch/octagon2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/design/canva2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/productivity/linear2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/productivity/todoist2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/search/exa2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/mcp-examples/search/serper2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/model-parameters2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/privacy2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/privacy-policy2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/quickstart2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/anthropic2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/cohere2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/google2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/groq2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/huggingface2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/mistralai2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/openai2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/remote-models/openrouter2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/server-examples/continue-dev2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/server-examples/llmcord2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/server-examples/n8n2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/server-examples/tabby2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/server-settings2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/server-troubleshooting2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/settings2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/desktop/troubleshooting2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference-administration2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference-authentication2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference-chat2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference-chat-conversations2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference-conversations2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference-jan-responses2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/api-reference-jan-server2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/architecture2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/configuration2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/development2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/installation2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/docs/server/overview2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/download2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/handbook2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/handbook/betting-on-open-source2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/handbook/open-superintelligence2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/benchmarking-nvidia-tensorrt-llm2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/bitdefender2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/data-is-moat2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/deepresearch2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/deepseek-r1-locally2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/jan-v1-for-research2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/offline-chatgpt-alternative2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/qwen3-settings2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/rag-is-not-enough2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/run-ai-models-locally2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/post/run-gpt-oss-locally2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/privacy2025-09-24T03:40:05.492Zdaily1 -https://jan.ai/support2025-09-24T03:40:05.492Zdaily1 - \ No newline at end of file diff --git a/docs/src/components/Blog/index.tsx b/docs/src/components/Blog/index.tsx index ca10cf408..16899df2c 100644 --- a/docs/src/components/Blog/index.tsx +++ b/docs/src/components/Blog/index.tsx @@ -19,6 +19,10 @@ const Blog = () => { name: 'Research', id: 'research', }, + { + name: 'Guides', + id: 'guides', + }, ] return ( diff --git a/docs/src/components/FooterMenu/index.tsx b/docs/src/components/FooterMenu/index.tsx index 68e1e6e78..317def75f 100644 --- a/docs/src/components/FooterMenu/index.tsx +++ b/docs/src/components/FooterMenu/index.tsx @@ -16,7 +16,10 @@ const FOOTER_MENUS: FooterMenu[] = [ { title: 'Company', links: [ - { name: 'Vision', href: '/', comingSoon: true }, + { + name: 'Open Superintelligence', + href: '/handbook/why/open-superintelligence', + }, { name: 'Handbook', href: '/handbook' }, { name: 'Community', href: 'https://discord.com/invite/FTk2MvZwJH' }, { name: 'Careers', href: 'https://menlo.bamboohr.com/careers' }, diff --git a/docs/src/components/Navbar.tsx b/docs/src/components/Navbar.tsx index 51044e9c7..b12fcaa73 100644 --- a/docs/src/components/Navbar.tsx +++ b/docs/src/components/Navbar.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/router' import { cn } from '@/lib/utils' import { FaDiscord, FaGithub } from 'react-icons/fa' import { FiDownload } from 'react-icons/fi' -import { FaXTwitter } from 'react-icons/fa6' +import { FaXTwitter, FaLinkedinIn } from 'react-icons/fa6' import { Button } from './ui/button' import LogoJanSVG from '@/assets/icons/logo-jan.svg' @@ -113,6 +113,43 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => { + +
  • + +
  • @@ -232,6 +269,14 @@ const Navbar = ({ noScroll }: { noScroll?: boolean }) => { > + + + +} + +const StatusIcon = ({ status }: { status: string }) => { + switch (status) { + case 'operational': + return + case 'degraded': + case 'partial_outage': + return + case 'major_outage': + return + case 'under_maintenance': + return + default: + return + } +} + +const getStatusColor = (status: string) => { + switch (status) { + case 'operational': + return 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800' + case 'degraded': + case 'partial_outage': + return 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-300 dark:border-yellow-800' + case 'major_outage': + return 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-300 dark:border-red-800' + case 'under_maintenance': + return 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800' + default: + return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/20 dark:text-gray-300 dark:border-gray-800' + } +} + +const getStatusText = (status: string) => { + switch (status) { + case 'operational': + return 'All Systems Operational' + case 'degraded': + return 'Degraded Performance' + case 'partial_outage': + return 'Partial Service Outage' + case 'major_outage': + return 'Major Service Outage' + case 'under_maintenance': + return 'Under Maintenance' + default: + return 'Status Unknown' + } +} + +export const OpenAIStatusChecker: React.FC = () => { + const [statusData, setStatusData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [lastRefresh, setLastRefresh] = useState(new Date()) + + const fetchStatus = useCallback(async () => { + setLoading(true) + setError(null) + + try { + console.log('Fetching real OpenAI status...') + + // Use CORS proxy to fetch real OpenAI status + const proxyUrl = 'https://api.allorigins.win/get?url=' + const targetUrl = 'https://status.openai.com/api/v2/status.json' + + const response = await fetch(proxyUrl + encodeURIComponent(targetUrl)) + + if (!response.ok) { + throw new Error(`Proxy returned ${response.status}`) + } + + const proxyData = await response.json() + const openaiData = JSON.parse(proxyData.contents) + + console.log('Real OpenAI data received:', openaiData) + + // Transform real OpenAI data to our format + const transformedData: StatusData = { + status: mapOpenAIStatusClient( + openaiData.status?.indicator || 'operational' + ), + lastUpdated: openaiData.page?.updated_at || new Date().toISOString(), + incidents: (openaiData.incidents || []).slice(0, 3), + } + + setStatusData(transformedData) + setLastRefresh(new Date()) + console.log('✅ Real OpenAI status loaded successfully!') + } catch (err) { + console.error('Failed to fetch real status:', err) + + // Fallback: try alternative proxy + try { + console.log('Trying alternative proxy...') + const altResponse = await fetch( + `https://cors-anywhere.herokuapp.com/https://status.openai.com/api/v2/summary.json` + ) + + if (altResponse.ok) { + const altData = await altResponse.json() + setStatusData({ + status: mapOpenAIStatusClient( + altData.status?.indicator || 'operational' + ), + lastUpdated: new Date().toISOString(), + incidents: [], + }) + setLastRefresh(new Date()) + console.log('✅ Alternative proxy worked!') + return + } + } catch (altErr) { + console.log('Alternative proxy also failed') + } + + // Final fallback + setError('Unable to fetch real-time status') + setStatusData({ + status: 'operational' as const, + lastUpdated: new Date().toISOString(), + incidents: [], + }) + setLastRefresh(new Date()) + console.log('Using fallback status') + } finally { + setLoading(false) + } + }, []) + + // Client-side status mapping function + const mapOpenAIStatusClient = (indicator: string): StatusData['status'] => { + switch (indicator.toLowerCase()) { + case 'none': + case 'operational': + return 'operational' + case 'minor': + return 'degraded' + case 'major': + return 'partial_outage' + case 'critical': + return 'major_outage' + case 'maintenance': + return 'under_maintenance' + default: + return 'operational' as const // Default to operational + } + } + + useEffect(() => { + fetchStatus() + // Refresh every 2 minutes for more real-time updates + const interval = setInterval(fetchStatus, 2 * 60 * 1000) + return () => clearInterval(interval) + }, [fetchStatus]) + + const handleRefresh = () => { + fetchStatus() + } + + if (loading && !statusData) { + return ( +
    +
    + + + Checking OpenAI Status... + +
    +
    + ) + } + + if (error) { + return ( +
    +
    +
    + +
    +

    + Unable to Check Status +

    +

    {error}

    +
    +
    + +
    +
    + ) + } + + return ( +
    + ) +} diff --git a/docs/src/pages/api-reference/installation.mdx b/docs/src/pages/api-reference/installation.mdx index de0609a08..266962089 100644 --- a/docs/src/pages/api-reference/installation.mdx +++ b/docs/src/pages/api-reference/installation.mdx @@ -3,7 +3,7 @@ title: Installation description: Install and deploy Jan Server on Kubernetes using minikube and Helm. --- -## Prerequisites +# Prerequisites Jan Server requires the following tools installed on your system: diff --git a/docs/src/pages/docs/_meta.json b/docs/src/pages/docs/_meta.json index 5b6962032..ee07f5d54 100644 --- a/docs/src/pages/docs/_meta.json +++ b/docs/src/pages/docs/_meta.json @@ -9,7 +9,7 @@ }, "desktop": { "type": "page", - "title": "Jan Desktop & Mobile" + "title": "Jan Desktop" }, "server": { "type": "page", diff --git a/docs/src/pages/docs/desktop/_meta.json b/docs/src/pages/docs/desktop/_meta.json index 36c70cf27..1745297cb 100644 --- a/docs/src/pages/docs/desktop/_meta.json +++ b/docs/src/pages/docs/desktop/_meta.json @@ -42,6 +42,5 @@ }, "settings": "Settings", "data-folder": "Jan Data Folder", - "troubleshooting": "Troubleshooting", - "privacy": "Privacy" + "troubleshooting": "Troubleshooting" } diff --git a/docs/src/pages/docs/desktop/index.mdx b/docs/src/pages/docs/desktop/index.mdx index a6ebed221..852f097a5 100644 --- a/docs/src/pages/docs/desktop/index.mdx +++ b/docs/src/pages/docs/desktop/index.mdx @@ -22,228 +22,52 @@ keywords: import { Callout } from 'nextra/components' import FAQBox from '@/components/FaqBox' -# Jan - -![Jan's Cover Image](./_assets/jan-app-new.png) - -## Jan's Goal - -> We're working towards open superintelligence to make a viable open-source alternative to platforms like ChatGPT -and Claude that anyone can own and run. - -## What is Jan Today - -Jan is an open-source AI platform that runs on your hardware. We believe AI should be in the hands of many, not -controlled by a few tech giants. - -Today, Jan is: -- **A desktop app** that runs AI models locally or connects to cloud providers -- **A model hub** making the latest open-source models accessible -- **A connector system** that lets AI interact with real-world tools via MCP - -Tomorrow, Jan aims to be a complete ecosystem where open models rival or exceed closed alternatives. +# Overview -We're building this with the open-source AI community, using the best available tools, and sharing everything -we learn along the way. +We're building [Open Superintelligence](https://jan.ai/handbook/open-superintelligence) together. -## The Jan Ecosystem +Jan is an open-source replacement for ChatGPT: +- AI Models: Use AI models with agentic capabilities + - [Open-source Models](/docs/desktop/manage-models): Run open-source locally + - [Cloud Models](/docs/desktop/remote-models/anthropic): Connect to remote models with API keys +- [Assistants](/docs/desktop/assistants): Create custom AI assistants +- [MCP Servers](/docs/desktop/mcp): Integrate MCP Servers to give agentic capabilities to AI models +- Jan Hub: Browse, install, and [manage models](/docs/desktop/manage-models) +- Local API Server: Expose an [OpenAI-compatible API](/docs/desktop/api-server) from your own machine or server -### Jan Apps -**Available Now:** -- **Desktop**: Full-featured AI workstation for Windows, Mac, and Linux +## Product Suite -**Coming Late 2025:** -- **Mobile**: Jan on your phone -- **Web**: Browser-based access at jan.ai -- **Server**: Self-hosted for teams -- **Extensions**: Browser extension for Chrome-based browsers +Jan is a full [product suite](https://en.wikipedia.org/wiki/Software_suite) that offers an alternative to Big AI: +- [Jan Desktop](/docs/desktop/quickstart): macOS, Windows, and Linux apps with offline mode +- [Jan Web](https://chat.jan.ai): Jan on browser, a direct alternative to chatgpt.com +- Jan Mobile: iOS and Android apps (Coming Soon) +- [Jan Server](/docs/server): deploy locally, in your cloud, or on-prem +- [Jan Models](/docs/models): Open-source models optimized for deep research, tool use, and reasoning -### Jan Model Hub -Making open-source AI accessible to everyone: -- **Easy Downloads**: One-click model installation -- **Jan Models**: Our own models optimized for local use - - **Jan-v1**: 4B reasoning model specialized in web search - - **Research Models** - - **Jan-Nano (32k/128k)**: 4B model for web search with MCP tools - - **Lucy**: 1.7B mobile-optimized for web search -- **Community Models**: Any GGUF from Hugging Face works in Jan -- **Cloud Models**: Connect your API keys for OpenAI, Anthropic, Gemini, and more +### Extending Jan (Coming Soon) +Jan helps you customize and align Open Superintelligence: +- Jan Connectors: Extend Jan with integrations +- Jan Studio: Fine-tune, align, and guardrail +- Evals: Benchmark models across industries, regions, and alignment dimensions +## Principles -### Jan Connectors Hub -Connect AI to the tools you use daily via [Model Context Protocol](./mcp): +- [Open source](https://www.redhat.com/en/blog/open-source-culture-9-core-principles-and-values): [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) licensed, built in public. +- No [vendor lock-in](https://en.wikipedia.org/wiki/Vendor_lock-in): Switch freely between local and frontier models. +- [Right to Repair](https://en.wikipedia.org/wiki/Right_to_repair): Inspect, audit, and modify your AI stack. -**Creative & Design:** -- **Canva**: Generate and edit designs - -**Data & Analysis:** -- **Jupyter**: Run Python notebooks -- **E2B**: Execute code in sandboxes - -**Web & Search:** -- **Browserbase & Browser Use**: Browser automation -- **Exa, Serper, Perplexity**: Advanced web search -- **Octagon**: Deep research capabilities - -**Productivity:** -- **Linear**: Project management -- **Todoist**: Task management - -## Core Features - -- **Run Models Locally**: Download any GGUF model from Hugging Face, use OpenAI's gpt-oss models, -or connect to cloud providers -- **OpenAI-Compatible API**: Local server at `localhost:1337` works with tools like -[Continue](./server-examples/continue-dev) and [Cline](https://cline.bot/) -- **Extend with MCP Tools**: Browser automation, web search, data analysis, and design tools, all -through natural language -- **Your Choice of Infrastructure**: Run on your laptop, self-host on your servers (soon), or use -cloud when you need it - -## Philosophy - -Jan is built to be user-owned: -- **Open Source**: Apache 2.0 license -- **Local First**: Your data stays on your device. Internet is optional -- **Privacy Focused**: We don't collect or sell user data. See our [Privacy Policy](./privacy) -- **No Lock-in**: Export your data anytime. Use any model. Switch between local and cloud - - -The best AI is the one you control. Not the one that others control for you. - - -## The Path Forward - -### What Works Today -- Run powerful models locally on consumer hardware -- Connect to any cloud provider with your API keys -- Use MCP tools for real-world tasks -- Access transparent model evaluations - -### What We're Building -- More specialized models that excel at specific tasks -- Expanded app ecosystem (mobile, web, extensions) -- Richer connector ecosystem -- An evaluation framework to build better models - -### The Long-Term Vision -We're working towards open superintelligence where: -- Open models match or exceed closed alternatives -- Anyone can run powerful AI on their own hardware -- The community drives innovation, not corporations -- AI capabilities are owned by users, not rented - - -This is an ambitious goal without a guaranteed path. We're betting on the open-source community, improved -hardware, and better techniques, but we're honest that this is a journey, not a destination we've reached. - - -## Quick Start - -1. [Download Jan](./quickstart) for your operating system -2. Choose a model - download locally or add cloud API keys -3. Start chatting or connect tools via MCP -4. Build with our [local API](./api-server) +Jan grows through contribution. It is shaped by many and belongs to everyone who uses it. ## Acknowledgements -Jan is built on the shoulders of giants: -- [Llama.cpp](https://github.com/ggerganov/llama.cpp) for inference -- [Model Context Protocol](https://modelcontextprotocol.io) for tool integration -- The open-source community that makes this possible +> Good artists copy, great artists steal. -## FAQs +Jan exists because we've borrowed, learned, and built on the work of others. - - Jan is an open-source AI platform working towards a viable alternative to Big Tech AI. Today it's a desktop app that runs models locally or connects to cloud providers. Tomorrow it aims to be a complete ecosystem rivaling platforms like ChatGPT and Claude. - - - - Other platforms are models behind APIs you rent. Jan is a complete AI ecosystem you own. Run any model, use real tools through MCP, keep your data private, and never pay subscriptions for local use. - - - - **Jan Models:** - - Jan-Nano (32k/128k) - Research and analysis with MCP integration - - Lucy - Mobile-optimized search (1.7B) - - Jan-v1 - Reasoning and tool use (4B) - - **Open Source:** - - OpenAI's gpt-oss models (120b and 20b) - - Any GGUF model from Hugging Face - - **Cloud (with your API keys):** - - OpenAI, Anthropic, Mistral, Groq, and more - - - - MCP (Model Context Protocol) lets AI interact with real applications. Instead of just generating text, your AI can create designs in Canva, analyze data in Jupyter, browse the web, and execute code - all through conversation. - - - - **Supported OS**: - - [Windows 10+](/docs/desktop/install/windows#compatibility) - - [macOS 12+](/docs/desktop/install/mac#compatibility) - - [Linux (Ubuntu 20.04+)](/docs/desktop/install/linux) - - **Hardware**: - - Minimum: 8GB RAM, 10GB storage - - Recommended: 16GB RAM, GPU (NVIDIA/AMD/Intel/Apple), 50GB storage - - - - Honestly? It's ambitious and uncertain. We believe the combination of rapidly improving open models, better consumer hardware, community innovation, and specialized models working together can eventually rival closed platforms. But this is a multi-year journey with no guarantees. What we can guarantee is that we'll keep building in the open, with the community, towards this goal. - - - - Right now, Jan can: - - Run models like Llama, Mistral, and our own Jan models locally - - Connect to cloud providers if you want more power - - Use MCP tools to create designs, analyze data, browse the web, and more - - Work completely offline once models are downloaded - - Provide an OpenAI-compatible API for developers - - - - **Local use**: Always free, no catches - **Cloud models**: You pay providers directly (we add no markup) - **Jan cloud**: Optional paid services coming 2025 - - The core platform will always be free and open source. - - - - - Runs 100% offline once models are downloaded - - All data stored locally in [Jan Data Folder](/docs/desktop/data-folder) - - No telemetry without explicit consent - - Open source code you can audit - - - When using cloud providers through Jan, their privacy policies apply. - - - - - Yes. Download directly or build from [source](https://github.com/menloresearch/jan). Jan Server for production deployments coming late 2025. - - - - - **Jan Web**: Beta late 2025 - - **Jan Mobile**: Late 2025 - - **Jan Server**: Late 2025 - - All versions will sync seamlessly. - - - - - Code: [GitHub](https://github.com/menloresearch/jan) - - Community: [Discord](https://discord.gg/FTk2MvZwJH) - - Testing: Help evaluate models and report bugs - - Documentation: Improve guides and tutorials - - - - Yes! We love hiring from our community. Check [Careers](https://menlo.bamboohr.com/careers). - +- [llama.cpp](https://github.com/ggerganov/llama.cpp) and [GGML](https://github.com/ggerganov/ggml) for efficient inference +- [r/LocalLLaMA](https://www.reddit.com/r/LocalLLaMA/) for ideas, feedback, and debate +- [Model Context Protocol](https://modelcontextprotocol.io) for MCP integrations +- [PostHog](https://posthog.com/docs) for docs inspiration +- The open-source community for contributions, bug reports, and improvements diff --git a/docs/src/pages/docs/desktop/install/linux.mdx b/docs/src/pages/docs/desktop/install/linux.mdx index 2d42a59f1..2d9d39f9f 100644 --- a/docs/src/pages/docs/desktop/install/linux.mdx +++ b/docs/src/pages/docs/desktop/install/linux.mdx @@ -1,11 +1,12 @@ --- title: Linux -description: Get started quickly with Jan, an AI chat application that runs 100% offline on your desktop & mobile (*coming soon*). +description: Download Jan on Linux to run AI models locally. Jan is a free, open-source ChatGPT alternative to run offline. keywords: [ Jan, Customizable Intelligence, LLM, local AI, + Jan on Linux, privacy focus, free and open source, private and offline, @@ -18,15 +19,17 @@ keywords: installation, "desktop" ] +twitter: + card: summary_large_image + site: "@jandotai" + title: "Jan on Linux" + description: "Download Jan on Linux to run AI models locally. Jan is a free, open-source ChatGPT alternative to run offline." --- - import FAQBox from '@/components/FaqBox' import { Tabs, Callout, Steps } from 'nextra/components' import { Settings } from 'lucide-react' - - # Linux Installation Instructions for installing Jan on Linux. @@ -244,7 +247,7 @@ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64 ### Step 2: Enable GPU Acceleration 1. Navigate to **Settings** () > **Local Engine** > **Llama.cpp** -2. Select appropriate backend in **llama-cpp Backend**. Details in our [guide](/docs/desktop/local-engines/llama-cpp). +2. Select appropriate backend in **llama-cpp Backend**. Details in our [llama.cpp guide](/docs/desktop/llama-cpp). CUDA offers better performance than Vulkan. diff --git a/docs/src/pages/docs/desktop/install/mac.mdx b/docs/src/pages/docs/desktop/install/mac.mdx index 827329d6e..b784f2ecf 100644 --- a/docs/src/pages/docs/desktop/install/mac.mdx +++ b/docs/src/pages/docs/desktop/install/mac.mdx @@ -1,11 +1,11 @@ --- title: Mac -description: Get started quickly with Jan - a local AI that runs on your computer. Install Jan and pick your model to start chatting. -keywords: +description: Download Jan on Mac to run AI models locally. Jan is a free, open-source ChatGPT alternative to run offline. [ Jan, Customizable Intelligence, LLM, local AI, + Jan on Mac, privacy focus, free and open source, private and offline, @@ -18,6 +18,11 @@ keywords: installation, "desktop" ] +twitter: + card: summary_large_image + site: "@jandotai" + title: "Jan on Mac" + description: "Download Jan on Mac to run AI models locally. Jan is a free, open-source ChatGPT alternative to run offline." --- import { Tabs } from 'nextra/components' diff --git a/docs/src/pages/docs/desktop/install/windows.mdx b/docs/src/pages/docs/desktop/install/windows.mdx index 2c56e2319..6e858a2b5 100644 --- a/docs/src/pages/docs/desktop/install/windows.mdx +++ b/docs/src/pages/docs/desktop/install/windows.mdx @@ -1,10 +1,11 @@ --- title: Windows -description: Run AI models locally on your Windows machine with Jan. Quick setup guide for local inference and chat. +description: Download Jan on Windows to run AI models locally. Jan is a free, open-source ChatGPT alternative to run offline. keywords: [ Jan, Customizable Intelligence, LLM, + Jan on Windows, local AI, privacy focus, free and open source, @@ -18,6 +19,11 @@ keywords: installation, "desktop" ] +twitter: + card: summary_large_image + site: "@jandotai" + title: "Jan on Windows" + description: "Download Jan on Windows to run AI models locally. Jan is a free, open-source ChatGPT alternative to run offline." --- import { Tabs, Callout, Steps } from 'nextra/components' diff --git a/docs/src/pages/docs/desktop/jan-models/jan-nano-32.mdx b/docs/src/pages/docs/desktop/jan-models/jan-nano-32.mdx index b216f3b96..5f1446e42 100644 --- a/docs/src/pages/docs/desktop/jan-models/jan-nano-32.mdx +++ b/docs/src/pages/docs/desktop/jan-models/jan-nano-32.mdx @@ -59,7 +59,7 @@ The model and its different model variants are fully supported by Jan. ## Using Jan-Nano-32k **Step 1** -Download Jan from [here](https://jan.ai/docs/desktop/). +Download Jan from [here](https://jan.ai/download/). **Step 2** Go to the Hub Tab, search for Jan-Nano-Gguf, and click on the download button to the best model size for your system. @@ -118,8 +118,8 @@ Here are some example queries to showcase Jan-Nano's web search capabilities: - 4xA6000 for vllm server (inferencing) - What frontend should I use? - - Jan Beta (recommended) - Minimalistic and polished interface - - Download link: https://jan.ai/docs/desktop/beta + - Jan (recommended) + - Download link: https://jan.ai/download - Getting Jinja errors in LM Studio? - Use Qwen3 template from other LM Studio compatible models diff --git a/docs/src/pages/docs/desktop/server-examples/tabby.mdx b/docs/src/pages/docs/desktop/server-examples/tabby.mdx index 917f40550..f25c89dab 100644 --- a/docs/src/pages/docs/desktop/server-examples/tabby.mdx +++ b/docs/src/pages/docs/desktop/server-examples/tabby.mdx @@ -90,7 +90,7 @@ Refer to the following documentation to install the Tabby extension on your favo Tabby offers an [Answer Engine](https://tabby.tabbyml.com/docs/administration/answer-engine/) on the homepage, which can leverage the Jan LLM and related contexts like code, documentation, and web pages to answer user questions. -Simply open the Tabby homepage at [localhost:8080](http://localhost:8080) and ask your questions. +Simply open the Tabby homepage at http://localhost:8080 and ask your questions. ### IDE Chat Sidebar diff --git a/docs/src/pages/docs/desktop/settings.mdx b/docs/src/pages/docs/desktop/settings.mdx index 6bc750f43..cd4d01ede 100644 --- a/docs/src/pages/docs/desktop/settings.mdx +++ b/docs/src/pages/docs/desktop/settings.mdx @@ -108,7 +108,7 @@ You can help improve Jan by sharing anonymous usage data: 2. You can change this setting at any time -Read more about that we collect with opt-in users at [Privacy](/docs/desktop/privacy). +Read more about that we collect with opt-in users at [Privacy](/privacy).
    @@ -141,7 +141,7 @@ This action cannot be undone. ### Jan Data Folder -Jan stores your data locally in your own filesystem in a universal file format. See detailed [Jan Folder Structure](docs/data-folder#folder-structure). +Jan stores your data locally in your own filesystem in a universal file format. See detailed [Jan Folder Structure](/docs/desktop/data-folder#directory-structure). **1. Open Jan Data Folder** diff --git a/docs/src/pages/docs/desktop/troubleshooting.mdx b/docs/src/pages/docs/desktop/troubleshooting.mdx index 16bbdfa9a..6d6c02703 100644 --- a/docs/src/pages/docs/desktop/troubleshooting.mdx +++ b/docs/src/pages/docs/desktop/troubleshooting.mdx @@ -328,14 +328,14 @@ This command ensures that the necessary permissions are granted for Jan's instal When you start a chat with a model and encounter a **Failed to Fetch** or **Something's Amiss** error, here are some possible solutions to resolve it: **1. Check System & Hardware Requirements** -- Hardware dependencies: Ensure your device meets all [hardware requirements](docs/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) -- OS: Ensure your operating system meets the minimum requirements ([Mac](/docs/desktop/install/mac#minimum-requirements), [Windows](/docs/desktop/install/windows#compatibility), [Linux](/docs/desktop/install/linux#compatibility)) +- Hardware dependencies: Ensure your device meets all [hardware requirements](troubleshooting) +- OS: Ensure your operating system meets the minimum requirements ([Mac](https://www.jan.ai/docs/desktop/install/mac#minimum-requirements), [Windows](/windows#compatibility), [Linux](https://www.jan.ai/docs/desktop/install/linux#compatibility) - RAM: Choose models that use less than 80% of your available RAM - For 8GB systems: Use models under 6GB - For 16GB systems: Use models under 13GB **2. Check Model Parameters** -- In **Engine Settings** in right sidebar, check your `ngl` ([number of GPU layers](/docs/desktop/models/model-parameters#engine-parameters)) setting to see if it's too high +- In **Engine Settings** in right sidebar, check your `ngl` ([number of GPU layers](/docs/desktop/model-parameters)) setting to see if it's too high - Start with a lower NGL value and increase gradually based on your GPU memory **3. Port Conflicts** diff --git a/docs/src/pages/handbook/_meta.json b/docs/src/pages/handbook/_meta.json index 482ca4f93..4a44a889f 100644 --- a/docs/src/pages/handbook/_meta.json +++ b/docs/src/pages/handbook/_meta.json @@ -1,5 +1,4 @@ { "index": "Overview", - "open-superintelligence": "Open Superintelligence", - "betting-on-open-source": "Betting on Open-Source" + "why": "Why does Jan exist?" } diff --git a/docs/src/pages/handbook/index.mdx b/docs/src/pages/handbook/index.mdx index 2c64eff72..64f0abdf8 100644 --- a/docs/src/pages/handbook/index.mdx +++ b/docs/src/pages/handbook/index.mdx @@ -18,31 +18,6 @@ Jan's Handbook is a [living document](https://en.wikipedia.org/wiki/Living_docum ## Why does Jan exist? -### [Open Superintelligence](/handbook/open-superintelligence) -Building superintelligence that belongs to everyone, not just a few tech giants. We believe the future of AI should be open, accessible, and owned by the people who use it. - -### [Betting on Open-Source](/handbook/betting-on-open-source) +- [Open Superintelligence](/handbook/open-superintelligence) - Building superintelligence that belongs to everyone, not just a few tech giants. We believe the future of AI should be open, accessible, and owned by the people who use it. +- [Betting on Open-Source](/handbook/betting-on-open-source) Why we're betting on open-source as the future of AI and technology. Open-source has consistently won in the long term, and AI will be no different. - ---- - -## Quick Links - -- **For the curious**: Start with [Open Superintelligence](/handbook/open-superintelligence) -- **For developers**: Learn about [Betting on Open-Source](/handbook/betting-on-open-source) -- **For contributors**: Check out our [GitHub](https://github.com/menloresearch/jan) and [Discord](https://discord.gg/FTk2MvZwJH) - -## Our North Star - -We're building superintelligence that: - -- **Works anywhere**: From your laptop to your data center -- **Belongs to you**: Download it, own it, modify it -- **Scales infinitely**: One person or ten thousand, same platform -- **Improves constantly**: Community-driven development - -This isn't just about making AI accessible. It's about ensuring the most transformative technology in human history can be owned by those who use it. - ---- - -_"The future of AI isn't about choosing between local or cloud. It's about having both, and everything in between, working perfectly together."_ diff --git a/docs/src/pages/handbook/why/_meta.json b/docs/src/pages/handbook/why/_meta.json new file mode 100644 index 000000000..b201b2e5f --- /dev/null +++ b/docs/src/pages/handbook/why/_meta.json @@ -0,0 +1,4 @@ +{ + "open-superintelligence": "Why Jan exists", + "betting-on-open-source": "Why we're betting on open-source" +} diff --git a/docs/src/pages/handbook/betting-on-open-source.mdx b/docs/src/pages/handbook/why/betting-on-open-source.mdx similarity index 73% rename from docs/src/pages/handbook/betting-on-open-source.mdx rename to docs/src/pages/handbook/why/betting-on-open-source.mdx index a0560d53e..bac971cbc 100644 --- a/docs/src/pages/handbook/betting-on-open-source.mdx +++ b/docs/src/pages/handbook/why/betting-on-open-source.mdx @@ -1,11 +1,11 @@ --- -title: "Why Open-Source" +title: "Why Jan is betting on Open-Source" description: "Why we're betting on open-source." --- # Why Open-Source -AI today is concentrated in the hands of a few companies. They ask for trust, while keeping the levers of control hidden. We think that's a mistake. +AI today is concentrated in the hands of [a few companies](https://stratechery.com/2025/tech-philosophy-and-ai-opportunity/). They ask for trust, while keeping the levers of control hidden. We think that's a mistake. When you depend on one vendor, your future is tied to their roadmap, their politics, their survival. If they get acquired, pivot, or shut down; you're stuck. @@ -16,9 +16,9 @@ Depending on a closed vendor means giving up more than flexibility: AI has become critical infrastructure. Nations, enterprises, even small teams rely on it to think and decide. And yet, control sits with a few vendors who decide the terms of access. We believe that's not control. That's dependency dressed up as convenience. One of the most powerful invention is being steered by a handful of executives. Their values shape what billions can say, build, or ask. -*This cannot stand. It must be changed.* +This can't stand. It must be changed. -## Jan's Bet +## How we see We don't believe the future of AI should be dictated by a few firms in San Francisco, Beijing, or anywhere else. @@ -30,4 +30,4 @@ That's why we're building Jan, a full product suite: - Jan Server - Hub, Store, evals, guardrails, the ecosystem around it -The goal is to be the open-source replacement for ChatGPT and other BigAI products, with models and tools you can run, own, and trust. +The goal is to be the [open-source replacement for ChatGPT](https://jan.ai/) and other BigAI products, with models and tools you can run, own, and trust. diff --git a/docs/src/pages/handbook/open-superintelligence.mdx b/docs/src/pages/handbook/why/open-superintelligence.mdx similarity index 67% rename from docs/src/pages/handbook/open-superintelligence.mdx rename to docs/src/pages/handbook/why/open-superintelligence.mdx index 5174f712b..68ab46311 100644 --- a/docs/src/pages/handbook/open-superintelligence.mdx +++ b/docs/src/pages/handbook/why/open-superintelligence.mdx @@ -5,9 +5,13 @@ description: "Short answer: Open Superintelligence." # Why does Jan exist? -> Short answer: Open Superintelligence. +import { Callout } from 'nextra/components' -In 1879, Edison lit a single street in [Menlo Park](https://en.wikipedia.org/wiki/Menlo_Park,_California). What mattered wasn't the bulb. It was that power could reach homes, schools, and factories. + +Short answer: Open Superintelligence. + + +In 1879, [Edison](https://en.wikipedia.org/wiki/Thomas_Edison) lit a single street in [Menlo Park](https://en.wikipedia.org/wiki/Menlo_Park,_California). What mattered wasn't the bulb. It was that power could reach homes, schools, and factories. Electricity changed the world only when it became universal. Standard plugs, cheap generation, lines everywhere. People stopped talking about electricity and started using light, cold chains, and machines. @@ -19,13 +23,13 @@ Jan exists to push intelligence toward the first path: Open Superintelligence yo > The world is made, and can be remade. -Every industrial wave redefined critical aspects of our daily lives: -- Factories introduced shift clocks and wage rhythms -- Steam gave way to electricity and standardized parts -- Rail, telegraph, and later networks changed how decisions travel -- Each wave pulled new bargains into being skills, schools, safety nets, labor law +Every industrial wave redefined new defaults of our daily lives: +- [Factories](https://en.wikipedia.org/wiki/Factory) created the modern job +- [Electricity](https://en.wikipedia.org/wiki/Electricity) created the modern home +- [Railroads](https://en.wikipedia.org/wiki/Rail_transport#History) and [telegraphs](https://en.wikipedia.org/wiki/Telegraphy#History) created the modern nation +- [The Internet](https://en.wikipedia.org/wiki/Internet) created the modern world -So what we're interested in is who is going to write the new defaults and share in the gains. +Open Superintelligence will create what comes next. What we're interested in is who is going to write the new defaults and share in the gains. Technology doesn’t choose its path, people do. Power accrues to whoever designs, deploys, and profits from the system: - If intelligence is closed and centralized, the gains concentrate diff --git a/docs/src/pages/post/_assets/claude.jpeg b/docs/src/pages/post/_assets/claude.jpeg new file mode 100644 index 000000000..1b5dc1158 Binary files /dev/null and b/docs/src/pages/post/_assets/claude.jpeg differ diff --git a/docs/src/pages/post/_assets/create-assistant-1.jpeg b/docs/src/pages/post/_assets/create-assistant-1.jpeg new file mode 100644 index 000000000..4c005b6ed Binary files /dev/null and b/docs/src/pages/post/_assets/create-assistant-1.jpeg differ diff --git a/docs/src/pages/post/_assets/gemini.jpeg b/docs/src/pages/post/_assets/gemini.jpeg new file mode 100644 index 000000000..acd333607 Binary files /dev/null and b/docs/src/pages/post/_assets/gemini.jpeg differ diff --git a/docs/src/pages/post/_assets/jan-assistant-for-law.png b/docs/src/pages/post/_assets/jan-assistant-for-law.png new file mode 100644 index 000000000..298d92f0f Binary files /dev/null and b/docs/src/pages/post/_assets/jan-assistant-for-law.png differ diff --git a/docs/src/pages/post/_assets/jan-for-ai-law-assistant-chat.jpeg b/docs/src/pages/post/_assets/jan-for-ai-law-assistant-chat.jpeg new file mode 100644 index 000000000..5501b5e70 Binary files /dev/null and b/docs/src/pages/post/_assets/jan-for-ai-law-assistant-chat.jpeg differ diff --git a/docs/src/pages/post/_assets/lm-studio.jpeg b/docs/src/pages/post/_assets/lm-studio.jpeg new file mode 100644 index 000000000..ee6ee3825 Binary files /dev/null and b/docs/src/pages/post/_assets/lm-studio.jpeg differ diff --git a/docs/src/pages/post/_assets/perplexity.jpeg b/docs/src/pages/post/_assets/perplexity.jpeg new file mode 100644 index 000000000..6a73f9b55 Binary files /dev/null and b/docs/src/pages/post/_assets/perplexity.jpeg differ diff --git a/docs/src/pages/post/_meta.json b/docs/src/pages/post/_meta.json index e5472dd61..1962ffa9e 100644 --- a/docs/src/pages/post/_meta.json +++ b/docs/src/pages/post/_meta.json @@ -20,5 +20,10 @@ "title": "Research", "display": "normal", "href": "/blog?category=research" + }, + "guides-cat": { + "title": "Guides", + "display": "normal", + "href": "/blog?category=guides" } } diff --git a/docs/src/pages/post/ai-for-law.mdx b/docs/src/pages/post/ai-for-law.mdx new file mode 100644 index 000000000..a42b70ced --- /dev/null +++ b/docs/src/pages/post/ai-for-law.mdx @@ -0,0 +1,123 @@ +--- +title: "Private AI for legal professionals who need confidentiality" +description: "It's possible to use AI without risking client data. Jan helps lawyers save time while keeping clients safe." +tags: AI, ai for law, ai for lawyers, ChatGPT alternative, Jan, local AI, offline AI +categories: guides +date: 2025-09-30 +ogImage: assets/images/general/jan-for-ai-law-assistant-chat.jpeg +twitter: + card: summary_large_image + site: "@jandotai" + title: "Private AI for legal professionals who need confidentiality" + description: "It's possible to use AI without risking client data. Jan helps lawyers save time while keeping clients safe." + image: assets/images/general/jan-assistants-ai-for-legal.jpeg +--- +import { Callout } from 'nextra/components' +import CTABlog from '@/components/Blog/CTA' +import { OpenAIStatusChecker } from '@/components/OpenAIStatusChecker' + +# Private AI for legal professionals who need confidentiality + +![AI for Law](/assets/images/general/jan-for-ai-law-assistant-chat.jpeg) + +Yes, it's possible to use AI in legal work without risking client data. + + +Client trust depends on privacy. Sending documents into public AI tools risks compliance and reputation. + + +Start by [downloading Jan](/download) and installing the **Jan v1 model**. Once installed, you can create assistants tailored to your practice and keep contracts, case notes, and client files under your control. + + +**Why use Jan for legal tasks** +- Runs locally on your hardware, no cloud uploads +- Keeps chats and interactions private +- Works offline once installed +- Lets you build assistants for your own workflows + + +--- + +## Create your assistant + +Once Jan is installed with the **Jan v1 model**, onboarding will guide you through downloading and setup. + +Click **Create assistant** to start: +![Create your first AI assistant in Jan](./_assets/create-assistant-1.jpeg) +*Create your first assistant in Jan* + +Add an assistant name and prompt: +![Jan assistant for contract review](./_assets/jan-assistant-for-law.png) +*Example of a Jan assistant for contract review* + +You can create assistants using specific prompts. Below are examples for common legal workflows. + +--- + +## Contract review assistant + +AI can help lawyers move faster through long contracts by pointing out what matters most. + +**Prompt for Jan:** +> You are a contract review assistant. +> When I paste a contract: +> - Highlight risky or unusual clauses +> - Flag ambiguous or missing terms +> - Summarize the agreement in plain English for a non-lawyer client +> Format your response with sections: **Risks**, **Ambiguities/Missing**, **Summary**. +> Do not provide legal advice. + +--- + +## Drafting assistant + +Use AI to create first drafts of NDAs, service agreements, or client letters. You still refine the output, but AI saves time on boilerplate. + +**Prompt for Jan:** +> You are a drafting assistant. +> When asked to draft a legal agreement or client letter: +> - Produce a professional first version +> - Use clear, concise language +> - Leave placeholders like [Party Name], [Date], [Amount] for details +> - Structure output with headings, numbered clauses, and consistent formatting +> Do not provide legal advice. + +--- + +## Case preparation assistant + +Case prep often means reading hundreds of pages. AI can turn depositions, discovery files, or judgments into concise notes. + +![Jan legal case preparation assistant](./_assets/jan-for-ai-law-assistant-chat.jpeg) +*Jan chat interface for case preparation — process documents and extract key information* + +**Prompt for Jan:** +> You are a case preparation assistant. +> When I provide case materials: +> - Extract key facts, issues, and arguments +> - Present them as bullet points under headings: **Facts**, **Issues**, **Arguments** +> - Keep summaries concise (under 500 words unless I request more) +> Use plain English, no speculation or legal conclusions. + +--- + +## Knowledge management assistant + +Law firms accumulate memos, policies, and precedents. AI can help organize and retrieve them quickly. + +**Prompt for Jan:** +> You are a knowledge management assistant. +> When I ask questions about internal documents: +> - Return concise summaries or direct excerpts +> - Always cite the source (e.g., “Policy Manual, Section 4”) +> - If not found in provided material, reply “Not found in documents.” +> Do not invent information. + +--- + +## Final note + +AI in legal practice is not about replacing lawyers. It’s about handling repetitive tasks safely so you can focus on real decisions. +With private AI, you gain efficiency without compromising client confidentiality. + + diff --git a/docs/src/pages/post/ai-for-teachers.mdx b/docs/src/pages/post/ai-for-teachers.mdx new file mode 100644 index 000000000..cd2c2b60b --- /dev/null +++ b/docs/src/pages/post/ai-for-teachers.mdx @@ -0,0 +1,134 @@ +--- +title: "AI for teachers who care about student privacy" +description: "Use AI in teaching without risking student data. Jan helps teachers plan lessons, grade faster, and communicate with parents." +tags: AI, ai for teachers, ChatGPT alternative, Jan, local AI, offline AI, education +categories: guides +date: 2025-10-01 +ogImage: assets/images/general/ai-for-teacher.jpeg +twitter: + card: summary_large_image + site: "@jandotai" + title: "AI for teachers who care about student privacy" + description: "Use AI in teaching without risking student data. Jan helps teachers plan lessons, grade faster, and communicate with parents." + image: assets/images/general/ai-for-teacher.jpeg +--- +import { Callout } from 'nextra/components' +import CTABlog from '@/components/Blog/CTA' + +# AI for teachers who care about student privacy + +![AI for teachers](/assets/images/general/ai-for-teacher.jpeg) + +AI can help teachers handle the work that piles up outside class. It can draft a lesson outline, suggest feedback on essays, or turn notes into a polite parent email. These are the tasks that usually stretch into evenings and weekends. + + +Most AI tools like ChatGPT run in the cloud. Sharing lesson plans, student writing, or parent details there risks compliance and trust. + + +That's where Jan comes in: +- [Download Jan](/download) +- You get the same time-saving help +- Your data never leaves your device. + + +*See how teachers use Jan for AI-powered lesson planning and grading* + + +**Why use Jan for teaching** +- Runs locally, no cloud servers +- Keeps lesson plans and student data private +- Works offline once installed +- Lets you build assistants for your daily teaching tasks + + +--- + +## Create your assistant + +Once Jan is installed, click **Create assistant** and add one of the prompts below. Each assistant is for a specific classroom task. + +![Create your first AI assistant in Jan](/assets/images/general/assistants-ai-for-teachers.jpeg) + +--- + +## Lesson planning assistant + +AI can draft lesson outlines in minutes. You adapt and refine them for your students. + +**Prompt for Jan:** +> You are a lesson planning assistant. +> When I give you a topic or subject: +> - Suggest a lesson outline with objectives, activities, and discussion questions +> - Adjust for different grade levels if I specify +> - Keep plans practical and realistic for a classroom setting + +Example ask: For Grade 6 science on ecosystems. Objectives: define food chains, explain producer/consumer roles. Activity: group poster on an ecosystem. Questions: How would removing one species affect the whole system? + +--- + +## Grading support assistant + +AI won't replace your judgment, but it can make feedback faster and more consistent. + +**Prompt for Jan:** +> You are a grading support assistant. +> When I paste student writing or answers: +> - Highlight strengths and areas for improvement +> - Suggest short, constructive feedback I can reuse +> - Keep tone supportive and professional +> Do not assign final grades. + +Example: For a history essay. Strength: clear thesis. Improvement: weak evidence. Feedback: "Great thesis and structure. Next time, support your points with specific historical examples." + +--- + +## Parent communication assistant + +Writing parent emails is important but time-consuming. + +**Prompt for Jan:** +> You are a parent communication assistant. +> When I give you key points about a student: +> - Draft a polite and empathetic email to parents +> - Use clear and professional language +> - Keep tone supportive, not overly formal +> Only include details I provide. + +Example: Notes: “Student is falling behind on homework, otherwise engaged in class.” - Output: a short, encouraging message suggesting a check-in at home. + +--- + +## Classroom resources assistant + +Generate quizzes, worksheets, or practice activities at short notice. + +**Prompt for Jan:** +> You are a classroom resource assistant. +> When I provide a topic or subject: +> - Generate sample quiz questions (multiple choice and short answer) +> - Suggest short practice activities +> - Provide answer keys separately +> Keep material age-appropriate for the level I specify. + +Example: For Grade 4 fractions. 5 multiple-choice questions with answer key, plus a quick worksheet with 3 practice problems. + +--- + +## Getting started + +1. [Download Jan](/download). +2. Install the Jan model (guided in-app) +3. Create your first assistant using one of the prompts above +4. Test with non-sensitive examples first +5. Use it in real classroom tasks once you're comfortable + +--- + +## Final note + +AI isn't here to replace teachers. It's here to take repetitive tasks off your plate so you can focus on teaching. With Jan, you can use AI confidently without risking student privacy. + + diff --git a/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx b/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx index 0d4bc9aa2..9fa67ea07 100644 --- a/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx +++ b/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx @@ -17,7 +17,7 @@ Jan now supports [NVIDIA TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) i We've been excited for TensorRT-LLM for a while, and [had a lot of fun implementing it](https://github.com/menloresearch/nitro-tensorrt-llm). As part of the process, we've run some benchmarks, to see how TensorRT-LLM fares on consumer hardware (e.g. [4090s](https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/), [3090s](https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/)) we commonly see in the [Jan's hardware community](https://discord.com/channels/1107178041848909847/1201834752206974996). - **Give it a try!** Jan's [TensorRT-LLM extension](/docs/desktop/built-in/tensorrt-llm) is available in Jan v0.4.9 and up ([see more](/docs/desktop/built-in/tensorrt-llm)). We precompiled some TensorRT-LLM models for you to try: `Mistral 7b`, `TinyLlama-1.1b`, `TinyJensen-1.1b` 😂 + **Give it a try!** Jan's TensorRT-LLM extension is available in Jan v0.4.9. We precompiled some TensorRT-LLM models for you to try: `Mistral 7b`, `TinyLlama-1.1b`, `TinyJensen-1.1b` 😂 Bugs or feedback? Let us know on [GitHub](https://github.com/menloresearch/jan) or via [Discord](https://discord.com/channels/1107178041848909847/1201832734704795688). diff --git a/docs/src/pages/post/chatgpt-alternatives.mdx b/docs/src/pages/post/chatgpt-alternatives.mdx new file mode 100644 index 000000000..36f44e5c3 --- /dev/null +++ b/docs/src/pages/post/chatgpt-alternatives.mdx @@ -0,0 +1,120 @@ +--- +title: "ChatGPT alternatives that actually replace it" +description: "See the best ChatGPT alternatives in 2025. We've listed tools that are alternatives to ChatGPT." +tags: AI, ChatGPT alternative, ChatGPT alternatives, alternative to chatgpt, Jan, local AI, privacy, open source, offline AI +categories: guides +date: 2025-09-29 +ogImage: assets/images/general/chatgpt-alternative-jan.jpeg +twitter: + card: summary_large_image + site: "@jandotai" + title: "ChatGPT alternatives that actually replace it." + description: "See the best ChatGPT alternatives in 2025. We've listed tools that are alternatives to ChatGPT." + image: assets/images/general/chatgpt-alternative-jan.jpeg +--- +import { Callout } from 'nextra/components' +import CTABlog from '@/components/Blog/CTA' + +# Best ChatGPT Alternatives + +ChatGPT works well, but it always needs internet, has usage limits, and isn't private. + +If you want options that fit different needs, offline use, privacy, or specialized tasks, see the best alternatives to ChatGPT available for specific use cases. + +## Comparison: ChatGPT Alternatives + +| ChatGPT Alternative | Offline | Key Strength | Best For | +| ------------------------- | ------- | ---------------------------- | -------------------------- | +| **[Jan](https://jan.ai)** | Yes | Runs Cloud + Offline, open-source | Best overall ChatGPT replacement | +| Claude | - | Strong writing and reasoning | Creative text & code | +| Gemini | - | Integrated with Google | Research tasks, image generation | +| Perplexity | - | Fast, with cited answers | Research and fact-checking | +| LM Studio | Yes | Runs open models on PC | Coding and experiments | + +### Jan is the best ChatGPT alternative + +![Use Jan to chat with AI models without internet access](/assets/images/general/chatgpt-alternative-jan.jpeg) +*Jan as an open-source alternative to ChatGPT* + +Jan is the most complete ChatGPT alternative available today. It enables: +- Use AI in online & offline (even on a plain) +- Agentic actions supported +- MCP servers supported for tools + +Unlike ChatGPT, it runs on your computer, which means: +- Offline AI capabilities (see [Offline ChatGPT post](https://www.jan.ai/post/offline-chatgpt-alternative) for details) +- 100% private +- Open-source & Free + + Jan is an [open-source replacement for ChatGPT.](https://www.jan.ai/) + +### Claude is the most notable online alternative +![Claude](./_assets/claude.jpeg) + +Claude has become the main online rival to ChatGPT. It stands out for writing, reasoning, and coding. + +- Handles very long documents and context well +- Strong for essays, research papers, and structured text +- Popular with developers for code explanations and debugging +- Cloud-only, no offline mode +- Filters outputs heavily, sometimes too restrictive + +### Gemini is the Google's integrated alternative +![Gemini](./_assets/gemini.jpeg) + +Gemini ties directly into Google’s apps and search. Great for users in the Google ecosystem. + +- Built into Gmail, Docs, and Google Search +- Good for real-time research and fact-checking +- Strong at pulling web context into answers +- Requires Google account, fully online +- Privacy concerns: all tied to Google services + +### Perplexity is the research-focused alternative +![Perplexity](./_assets/perplexity.jpeg) + +Perplexity is built for fact-checking and quick research, not creativity. + +- Always cites sources for answers +- Strong at summarizing current web info +- Very fast for Q&A style use +- Limited in creativity and open-ended writing +- Cloud-only, daily free usage caps + +### LM Studio is the experimental alternative + +![LM Studio](./_assets/lm-studio.jpeg) + +LM Studio is not a ChatGPT replacement but a local tool for running open models. + +- Lets you test and run open-source models on PC +- Offline by default, works without internet +- Flexible setup for developers and technical users +- Requires decent hardware (RAM/VRAM) + +LM Studio is not beginner-friendly compared to Jan. + +## Choosing the right ChatGPT alternative for you: + +- Best overall replacement: [Jan](https://www.jan.ai/) +- For writing & storytelling: Claude +- For research & web knowledge: Perplexity or Gemini +- For productivity & office work: Microsoft Copilot +- For experimentation with open-source models for technical people: LM Studio + +Most ChatGPT alternatives are still cloud-based and limited. If you want full privacy, offline use, and no restrictions, the best ChatGPT alternative is [Jan](https://www.jan.ai/). + +### Can I use ChatGPT offline? +No. ChatGPT always requires internet. For offline AI, use Jan. + +### What’s the best free ChatGPT alternative? +Jan is free, open-source, and runs offline. Others like Claude or Perplexity have limited free tiers but are cloud-based. + +### Which ChatGPT alternative is best for writing? +Claude is strong for essays, reports, and structured writing. You could use [open-source models](https://www.jan.ai/post/run-ai-models-locally) in Jan too. + +### Which ChatGPT alternative is best for research? +Perplexity and Gemini pull real-time web data with citations. + +### What’s the closest full replacement to ChatGPT? +Jan. It runs locally, works offline, and feels like ChatGPT without restrictions. \ No newline at end of file diff --git a/docs/src/pages/post/deepresearch.mdx b/docs/src/pages/post/deepresearch.mdx index 11edd4f04..50cfc19ad 100644 --- a/docs/src/pages/post/deepresearch.mdx +++ b/docs/src/pages/post/deepresearch.mdx @@ -4,13 +4,13 @@ description: "A simple guide to replicating Deep Research results for free, with tags: AI, local models, Jan, GGUF, Deep Research, local AI categories: guides date: 2025-08-04 -ogImage: _assets/research-result-local.png +ogImage: assets/images/general/research-result-local.png twitter: card: summary_large_image site: "@jandotai" title: "Replicating Deep Research with Jan" description: "Learn how to replicate Deep Research results with Jan." - image: _assets/research-result-local.jpg + image: assets/images/general/research-result-local.png --- import { Callout } from 'nextra/components' @@ -125,8 +125,8 @@ any version with Model Context Protocol in it (>`v0.6.3`). **The Key: Assistants + Tools** -Running deep research in Jan can be accomplished by combining [custom assistants](https://jan.ai/docs/assistants) -with [MCP search tools](https://jan.ai/docs/desktop/mcp-examples/search/exa). This pairing allows any model—local or +Running deep research in Jan can be accomplished by combining [custom assistants](https://jan.ai/docs/desktop/assistants) +with [MCP search tools](https://jan.ai/docs/mcp-examples/search/exa). This pairing allows any model—local or cloud—to follow a systematic research workflow, to create a report similar to that of other providers, with some visible limitations (for now). diff --git a/docs/src/pages/post/deepseek-r1-locally.mdx b/docs/src/pages/post/deepseek-r1-locally.mdx index c9fb229b5..6d09532e9 100644 --- a/docs/src/pages/post/deepseek-r1-locally.mdx +++ b/docs/src/pages/post/deepseek-r1-locally.mdx @@ -4,7 +4,7 @@ description: "A straightforward guide to running DeepSeek R1 locally regardless tags: DeepSeek, R1, local AI, Jan, GGUF, Qwen, Llama categories: guides date: 2025-01-31 -ogImage: assets/deepseek-r1-locally-jan.jpg +ogImage: assets/images/general/deepseek-r1-locally-jan.jpg twitter: card: summary_large_image site: "@jandotai" @@ -17,7 +17,7 @@ import CTABlog from '@/components/Blog/CTA' # Run DeepSeek R1 locally on your device (Beginner-Friendly Guide) -![DeepSeek R1 running locally in Jan AI interface, showing the chat interface and model settings](./_assets/deepseek-r1-locally-jan.jpg) +![DeepSeek R1 running locally in Jan AI interface, showing the chat interface and model settings](/assets/images/general/deepseek-r1-locally-jan.jpg) DeepSeek R1 is one of the best open-source models in the market right now, and you can run DeepSeek R1 on your own computer! diff --git a/docs/src/pages/post/how-we-benchmark-kernels.mdx b/docs/src/pages/post/how-we-benchmark-kernels.mdx index dca80b095..6d5f6d947 100644 --- a/docs/src/pages/post/how-we-benchmark-kernels.mdx +++ b/docs/src/pages/post/how-we-benchmark-kernels.mdx @@ -3,7 +3,7 @@ title: "How we (try to) benchmark GPU kernels accurately" description: "We present the process behind how we decided to benchmark GPU kernels and iteratively improved our benchmarking pipeline" tags: "" categories: research -ogImage: "./_assets/cover-kernel-benchmarking.png" +ogImage: assets/images/general/cover-kernel-benchmarking.png date: 2025-09-17 --- diff --git a/docs/src/pages/post/is-chatgpt-down-use-jan.mdx b/docs/src/pages/post/is-chatgpt-down-use-jan.mdx new file mode 100644 index 000000000..dfd9843fd --- /dev/null +++ b/docs/src/pages/post/is-chatgpt-down-use-jan.mdx @@ -0,0 +1,124 @@ +--- +title: "If ChatGPT is down, switch to AI that never goes down" +description: "Check if ChatGPT down right now, and learn how to use AI that never goes down." +tags: AI, ChatGPT down, ChatGPT alternative, Jan, local AI, offline AI, ChatGPT at capacity +categories: guides +date: 2025-09-30 +ogImage: assets/images/general/is-chatgpt-down.jpg +twitter: + card: summary_large_image + site: "@jandotai" + title: "Realtime Status: Is ChatGPT down?" + description: "Check if ChatGPT is down right now with our real-time status checker, and learn how to use AI that never goes offline." + image: assets/images/general/is-chatgpt-down.jpg +--- +import { Callout } from 'nextra/components' +import CTABlog from '@/components/Blog/CTA' +import { OpenAIStatusChecker } from '@/components/OpenAIStatusChecker' + +# If ChatGPT is down, switch to AI that never goes down + +If you're seeing ChatGPT is down, it could be a good signal to switch to [Jan](https://www.jan.ai/), AI that never goes down. + +## 🔴 Realtime Status: Is ChatGPT down? + +This live tracker shows if ChatGPT is down right now. + + + +### ChatGPT Status Indicators + +
    +
    +
    +
    + Operational +
    +

    All systems are functioning normally with no reported issues.

    +
    + +
    +
    +
    + Degraded Performance +
    +

    Services are running but may be slower than usual.

    +
    + +
    +
    +
    + Partial Outage +
    +

    Some features or regions may be experiencing issues.

    +
    + +
    +
    +
    + Major Outage +
    +

    Significant service disruption affecting most users.

    +
    +
    + +## Skip the downtime with Jan + +When ChatGPT is down, Jan keeps working. Jan is an open-source ChatGPT alternative that runs on your computer - no servers, no outages, no waiting. + +![Jan running when ChatGPT is down](/assets/images/general/is-chatgpt-down.jpg) +*Jan works even when ChatGPT doesn't.* + +### Why Jan never goes down: +- **Runs locally** - No dependency on external servers +- **Always available** - Works offline, even on flights +- **No capacity limits** - Uses your computer's resources +- **100% private** - Your conversations stay on your device + +### Get started in 3 mins: +1. Download Jan: [jan.ai](https://jan.ai) +2. Install a model: Choose from Jan, Qwen, or other top models +3. Start chatting: Similar design as ChatGPT, but always available if you use local models + + +**Pro tip:** Keep both ChatGPT and Jan. You'll never lose productivity to outages again. + + +Jan runs AI models locally, so you don't need internet access. That means Jan is unaffected when ChatGPT is down. + +### Why does ChatGPT goes down? +There could be multiple reasons: +- Too many users at once +- Data center or API downtime +- Planned or uplanned updates +- Limited in some locations + +ChatGPT depends on OpenAI’s servers. If those go down, so does ChatGPT. Jan users don't affect by ChatGPT's outage. + +### Common ChatGPT Errors + +When ChatGPT experiences issues, you might see these error messages: + +- "ChatGPT is at capacity right now": Too many users online, try again later +- "Error in message stream": Connection problems with OpenAI servers +- "Something went wrong": General server error, refresh and retry +- "Network error": Internet connectivity issues on your end or OpenAI's +- "Rate limit exceeded": Too many requests sent, wait before trying again +- "This model is currently overloaded": High demand for specific model + +## Quick answers about ChatGPT status + +### Is ChatGPT down? +Check the ChatGPT realtime status above. [See if ChatGPT is down right now.](http://localhost:3000/post/is-chatgpt-down-use-jan#-realtime-status-is-chatgpt-down) + +### Why is ChatGPT down? +Usually server overload, maintenance, or outages at OpenAI. + +### What does "ChatGPT is at capacity" mean? +Too many users are online at the same time. You’ll need to wait or switch to Jan instead. + +### Is ChatGPT shutting down? +No, ChatGPT isn't shutting down. Outages are temporary. + +### Can I use ChatGPT offline? +No. ChatGPT always requires internet. For [offline AI](https://www.jan.ai/post/offline-chatgpt-alternative), use [Jan](https://jan.ai). \ No newline at end of file diff --git a/docs/src/pages/post/offline-chatgpt-alternative.mdx b/docs/src/pages/post/offline-chatgpt-alternative.mdx index d73bec712..7f94cc23e 100644 --- a/docs/src/pages/post/offline-chatgpt-alternative.mdx +++ b/docs/src/pages/post/offline-chatgpt-alternative.mdx @@ -1,32 +1,36 @@ --- title: "Offline ChatGPT: You can't run ChatGPT offline, do this instead" -description: "Learn how to use AI offline with Jan - a free, open-source alternative to ChatGPT that works 100% offline on your computer." -tags: AI, ChatGPT alternative, offline AI, Jan, local AI, privacy +description: "Use offline AI with Jan, a free & open-source alternative to ChatGPT that runs 100% offline." +tags: AI, chatgpt offline, ChatGPT alternative, offline AI, Jan, local AI, privacy categories: guides date: 2025-02-08 -ogImage: _assets/offline-chatgpt-alternatives-jan.jpg +ogImage: assets/images/general/offline-chatgpt-alternatives-jan.jpg twitter: card: summary_large_image site: "@jandotai" title: "Offline ChatGPT: You can't run ChatGPT offline, do this instead" - description: "Want to use ChatGPT offline? Learn how to run AI models locally with Jan - free, open-source, and works without internet." - image: _assets/offline-chatgpt-alternatives-jan.jpg + description: "Use offline AI with Jan, a free & open-source alternative to ChatGPT that runs 100% offline." + image: assets/images/general/offline-chatgpt-alternatives-jan.jpg --- import { Callout } from 'nextra/components' import CTABlog from '@/components/Blog/CTA' # Offline ChatGPT: You can't run ChatGPT offline, do this instead -ChatGPT is a cloud-based service that requires internet access. However, it's not the only way to use AI. You can run AI models offline on your device with [Jan](https://jan.ai/). It's completely free, open-source, and gives you 100% offline capability. You can even use AI on a plane! +ChatGPT itself can't run offline. ChatGPT can't run offline. You can’t download it. It always needs internet, because it runs on OpenAI's servers. - +If you want offline AI, you need local models. The easiest way: [Jan, an open-source replacement of ChatGPT](https://jan.ai/). It's free, open-source, and works 100% offline. With Jan, you can even use AI on a plane. + + **Quick Summary:** -- ChatGPT always needs internet - it can't run offline -- Jan lets you run AI models 100% offline on your computer -- It's free and open-source -- Works on Mac, Windows, and Linux +- ChatGPT always needs internet - no offline mode +- Use Jan to use AI models 100% offline +- It's free & open-source, and works on Mac, Windows, and Linux +## How to use AI offline? +Offline AI means the model runs on your computer. So no internet needed, 100% private, and data never leaves your device. With Jan you can run offline AI models locally. + ## Jan as an offline ChatGPT alternative ![Use Jan to chat with AI models without internet access](./_assets/offline-chatgpt-alternative-ai-without-internet.jpg) @@ -42,23 +46,25 @@ Go to [jan.ai](https://jan.ai) and download the version for your computer (Mac, ### 2. Download an AI model -You'll need an AI model to use AI offline, so download a model from Jan. Once it's on your computer, you don't need internet anymore. +You'll need an AI model to use AI offline, so download a model from Jan. Once it's on your computer, you don't need internet anymore. You can also use GPT models via Jan - check [running gpt-oss locally](https://www.jan.ai/post/run-gpt-oss-locally) post to see it. ![Choose an AI model that works offline](./_assets/jan-model-selection.jpg "Find the perfect AI model for offline use") *Select an AI model that matches your needs and computer capabilities* - -**Which model should you choose?** +### Which model should you choose? - For most computers: Try Mistral 7B or DeepSeek - they're similar to ChatGPT 3.5 - For older computers: Use smaller 3B models - For gaming PCs: You can try larger 13B models + Don't worry about choosing - Jan will automatically recommend models that work well on your computer. +If you'd like to learn more about local AI, check [how to run AI models locally as a beginner](https://www.jan.ai/post/run-ai-models-locally) article. + ### 3. Start using AI offline -![Chat with AI offline using Jan's interface](./_assets/run-ai-locally-with-jan.jpg "Experience ChatGPT-like interactions without internet") +![Chat with AI offline using Jan's interface](/assets/images/general/run-ai-locally-with-jan.jpg "Experience ChatGPT-like interactions without internet") *Use Jan's clean interface to chat with AI - no internet required* Once downloaded, you can use AI anywhere, anytime: @@ -71,12 +77,7 @@ Once downloaded, you can use AI anywhere, anytime: ## How to chat with your docs in Jan? -To chat with your docs in Jan, you need to activate experimental mode. - -![Activate experimental mode in Jan's settings](./_assets/chat-with-your-docs-offline-ai.jpg "Enable experimental features to chat with your documents") -*Turn on experimental mode in settings to chat with your docs* - -After activating experimental mode, simply add your files and ask questions about them. +Simply add your files and ask questions about them. ![Chat with your documents using Jan](./_assets/chat-with-docs-prompt.jpg "Ask questions about your documents offline") *Chat with your documents privately - no internet needed* @@ -97,17 +98,17 @@ Local AI makes possible offline AI use, so Jan is going to be your first step to 4. **No Server Issues:** No more "ChatGPT is at capacity" 5. **Your Choice of Models:** Use newer models as they come out -**"Is it really free? What's the catch?"** +### "Is Jan really free? What's the catch?" Yes, it's completely free and open source. Jan is built by developers who believe in making AI accessible to everyone. -**"How does it compare to ChatGPT?"** +### How does Jan compare to ChatGPT?" Modern open-source models like DeepSeek and Mistral are very capable. While they might not match GPT-4, they're perfect for most tasks and getting better every month. -**"Do I need a powerful computer?"** +### "Do I need a powerful computer?" If your computer is from the last 5 years, it will likely work fine. You need about 8GB of RAM and 10GB of free space for comfortable usage. -**"What about my privacy?"** -Everything stays on your computer. Your conversations, documents, and data never leave your device unless you choose to share them. +### "What about my privacy?" +Everything stays on your computer with Jan. Your conversations, documents, and data never leave your device unless you choose to share them. Want to learn more about the technical side? Check our detailed [guide on running AI models locally](/post/run-ai-models-locally). It's not required to [use AI offline](https://jan.ai/) but helps understand how it all works. @@ -116,3 +117,20 @@ Want to learn more about the technical side? Check our detailed [guide on runnin [Join our Discord community](https://discord.gg/Exe46xPMbK) for support and tips on using Jan as your offline ChatGPT alternative. + +### FAQ + +#### Can I download ChatGPT for offline use? +No. ChatGPT is cloud-only. + +#### How to use ChatGPT offline? +You can't. ChatGPT has no offline mode. Use Jan instead for a ChatGPT-like offline experience. + +#### Does ChatGPT have internet access? +Yes. It runs in the cloud. + +#### What's the best way to use AI offline? +Download Jan and run models like Mistral, DeepSeek, or GPT-OSS locally. + +#### What's GPT offline? +OpenAI has open-source models you can run locally but not via ChatGPT. One of them is [gpt-oss](https://www.jan.ai/post/run-gpt-oss-locally) and you can run it via Jan. \ No newline at end of file diff --git a/docs/src/pages/post/qwen3-settings.mdx b/docs/src/pages/post/qwen3-settings.mdx index c4635451c..07af8b9ba 100644 --- a/docs/src/pages/post/qwen3-settings.mdx +++ b/docs/src/pages/post/qwen3-settings.mdx @@ -50,7 +50,7 @@ Thinking mode is powerful, but greedy decoding kills its output. It'll repeat or ## Quick summary -![Qwen3 settings](./_assets/qwen3-settings-jan-ai.jpeg) +![Qwen3 settings](/assets/images/general/qwen3-30b-settings.jpg) ### Non-thinking mode (`enable_thinking=False`) diff --git a/docs/src/pages/post/run-ai-models-locally.mdx b/docs/src/pages/post/run-ai-models-locally.mdx index efe8bc594..315d9aad2 100644 --- a/docs/src/pages/post/run-ai-models-locally.mdx +++ b/docs/src/pages/post/run-ai-models-locally.mdx @@ -4,7 +4,7 @@ description: "A straightforward guide to running AI models locally on your compu tags: AI, local models, Jan, GGUF, privacy, local AI categories: guides date: 2025-01-31 -ogImage: assets/run-ai-locally-with-jan.jpg +ogImage: assets/images/general/run-ai-locally-with-jan.jpg twitter: card: summary_large_image site: "@jandotai" @@ -35,7 +35,7 @@ Most people think running AI models locally is complicated. It's not. Anyone can That's all to run your first AI model locally! -![Jan's simple and clean chat interface for local AI](./_assets/run-ai-locally-with-jan.jpg "Jan's easy-to-use chat interface after installation") +![Jan's simple and clean chat interface for local AI](/assets/images/general/run-ai-locally-with-jan.jpg "Jan's easy-to-use chat interface after installation") *Jan's easy-to-use chat interface after installation.* Keep reading to learn key terms of local AI and the things you should know before running AI models locally. diff --git a/docs/src/pages/post/run-gpt-oss-locally.mdx b/docs/src/pages/post/run-gpt-oss-locally.mdx index 5f71e8b45..795738644 100644 --- a/docs/src/pages/post/run-gpt-oss-locally.mdx +++ b/docs/src/pages/post/run-gpt-oss-locally.mdx @@ -4,21 +4,19 @@ description: "Complete 5-minute beginner guide to running OpenAI's gpt-oss local tags: OpenAI, gpt-oss, local AI, Jan, privacy, Apache-2.0, llama.cpp, Ollama, LM Studio categories: guides date: 2025-08-06 -ogImage: assets/gpt-oss%20locally.jpeg +ogImage: assets/images/general/gpt-oss locally.jpeg twitter: card: summary_large_image site: "@jandotai" title: "Run OpenAI's gpt-oss Locally in 5 Minutes (Beginner Guide)" description: "Complete 5-minute beginner guide to running OpenAI's gpt-oss locally with Jan AI for private, offline conversations." - image: assets/gpt-oss%20locally.jpeg + image: assets/images/general/gpt-oss locally.jpeg --- import { Callout } from 'nextra/components' import CTABlog from '@/components/Blog/CTA' # Run OpenAI's gpt-oss Locally in 5 mins -![gpt-oss running locally in Jan interface](./_assets/gpt-oss%20locally.jpeg) - OpenAI launched [gpt-oss](https://openai.com/index/introducing-gpt-oss/), marking their return to open-source AI after GPT-2. This model is designed to run locally on consumer hardware. This guide shows you how to install and run gpt-oss on your computer for private, offline AI conversations. ## What is gpt-oss? diff --git a/docs/theme.config.tsx b/docs/theme.config.tsx index 8b71c4cca..f3d1ab69c 100644 --- a/docs/theme.config.tsx +++ b/docs/theme.config.tsx @@ -107,14 +107,15 @@ const config: DocsThemeConfig = { head: function useHead() { const { title, frontMatter } = useConfig() const { asPath } = useRouter() - const titleTemplate = - (asPath.includes('/desktop') + const titleTemplate = asPath.includes('/post/') + ? (frontMatter?.title || title) + : (asPath.includes('/desktop') ? 'Jan Desktop' : asPath.includes('/server') ? 'Jan Server' : 'Jan') + - ' - ' + - (frontMatter?.title || title) + ' - ' + + (frontMatter?.title || title) return ( diff --git a/extensions-web/package.json b/extensions-web/package.json index 232ba13fa..aa536e9fe 100644 --- a/extensions-web/package.json +++ b/extensions-web/package.json @@ -22,6 +22,9 @@ }, "devDependencies": { "@janhq/core": "workspace:*", + "@tabler/icons-react": "^3.34.0", + "@types/react": "19.1.2", + "react": "19.0.0", "typescript": "5.9.2", "vite": "5.4.20", "vitest": "2.1.9", @@ -29,6 +32,8 @@ }, "peerDependencies": { "@janhq/core": "*", + "@tabler/icons-react": "*", + "react": "19.0.0", "zustand": "5.0.3" }, "dependencies": { diff --git a/extensions-web/src/conversational-web/const.ts b/extensions-web/src/conversational-web/const.ts index 0ad7e9049..4a860c500 100644 --- a/extensions-web/src/conversational-web/const.ts +++ b/extensions-web/src/conversational-web/const.ts @@ -14,4 +14,4 @@ export const DEFAULT_ASSISTANT = { name: 'Jan', avatar: '👋', created_at: 1747029866.542, -} \ No newline at end of file +} diff --git a/extensions-web/src/conversational-web/extension.ts b/extensions-web/src/conversational-web/extension.ts index 7c31f1c31..0e01e2ca3 100644 --- a/extensions-web/src/conversational-web/extension.ts +++ b/extensions-web/src/conversational-web/extension.ts @@ -11,6 +11,9 @@ import { } from '@janhq/core' import { RemoteApi } from './api' import { getDefaultAssistant, ObjectParser, combineConversationItemsToMessages } from './utils' +import { ApiError } from '../shared/types/errors' + +const CONVERSATION_NOT_FOUND_EVENT = 'conversation-not-found' export default class ConversationalExtensionWeb extends ConversationalExtension { private remoteApi: RemoteApi | undefined @@ -111,6 +114,15 @@ export default class ConversationalExtensionWeb extends ConversationalExtension return messages } catch (error) { console.error('Failed to list messages:', error) + // Check if it's a 404 error (conversation not found) + if (error instanceof ApiError && error.isNotFound()) { + // Trigger a navigation event to redirect to home + // We'll use a custom event that the web app can listen to + window.dispatchEvent(new CustomEvent(CONVERSATION_NOT_FOUND_EVENT, { + detail: { threadId, error: error.message } + })) + } + return [] } } diff --git a/extensions-web/src/jan-provider-web/api.ts b/extensions-web/src/jan-provider-web/api.ts index 436ee06b6..97a9608f2 100644 --- a/extensions-web/src/jan-provider-web/api.ts +++ b/extensions-web/src/jan-provider-web/api.ts @@ -5,9 +5,45 @@ import { getSharedAuthService, JanAuthService } from '../shared' import { JanModel, janProviderStore } from './store' +import { ApiError } from '../shared/types/errors' // JAN_API_BASE is defined in vite.config.ts +// Constants +const TEMPORARY_CHAT_ID = 'temporary-chat' + +/** + * Determines the appropriate API endpoint and request payload based on chat type + * @param request - The chat completion request + * @returns Object containing endpoint URL and processed request payload + */ +function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) { + const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID + + // For temporary chats, use the stateless /chat/completions endpoint + // For regular conversations, use the stateful /conv/chat/completions endpoint + const endpoint = isTemporaryChat + ? `${JAN_API_BASE}/chat/completions` + : `${JAN_API_BASE}/conv/chat/completions` + + const payload = { + ...request, + stream, + ...(isTemporaryChat ? { + // For temporary chat: don't store anything, remove conversation metadata + conversation_id: undefined, + } : { + // For regular chat: store everything, use conversation metadata + store: true, + store_reasoning: true, + conversation: request.conversation_id, + conversation_id: undefined, + }) + } + + return { endpoint, payload, isTemporaryChat } +} + export interface JanModelsResponse { object: string data: JanModel[] @@ -102,7 +138,8 @@ export class JanApiClient { return models } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to fetch models' + const errorMessage = error instanceof ApiError ? error.message : + error instanceof Error ? error.message : 'Failed to fetch models' janProviderStore.setError(errorMessage) janProviderStore.setLoadingModels(false) throw error @@ -115,22 +152,18 @@ export class JanApiClient { try { janProviderStore.clearError() + const { endpoint, payload } = getChatCompletionConfig(request, false) + return await this.authService.makeAuthenticatedRequest( - `${JAN_API_BASE}/conv/chat/completions`, + endpoint, { method: 'POST', - body: JSON.stringify({ - ...request, - stream: false, - store: true, - store_reasoning: true, - conversation: request.conversation_id, - conversation_id: undefined, - }), + body: JSON.stringify(payload), } ) } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to create chat completion' + const errorMessage = error instanceof ApiError ? error.message : + error instanceof Error ? error.message : 'Failed to create chat completion' janProviderStore.setError(errorMessage) throw error } @@ -144,23 +177,17 @@ export class JanApiClient { ): Promise { try { janProviderStore.clearError() - + const authHeader = await this.authService.getAuthHeader() - - const response = await fetch(`${JAN_API_BASE}/conv/chat/completions`, { + const { endpoint, payload } = getChatCompletionConfig(request, true) + + const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader, }, - body: JSON.stringify({ - ...request, - stream: true, - store: true, - store_reasoning: true, - conversation: request.conversation_id, - conversation_id: undefined, - }), + body: JSON.stringify(payload), }) if (!response.ok) { @@ -216,7 +243,8 @@ export class JanApiClient { reader.releaseLock() } } catch (error) { - const err = error instanceof Error ? error : new Error('Unknown error occurred') + const err = error instanceof ApiError ? error : + error instanceof Error ? error : new Error('Unknown error occurred') janProviderStore.setError(err.message) onError?.(err) throw err @@ -230,7 +258,8 @@ export class JanApiClient { await this.getModels() console.log('Jan API client initialized successfully') } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to initialize API client' + const errorMessage = error instanceof ApiError ? error.message : + error instanceof Error ? error.message : 'Failed to initialize API client' janProviderStore.setError(errorMessage) throw error } finally { @@ -239,4 +268,4 @@ export class JanApiClient { } } -export const janApiClient = JanApiClient.getInstance() \ No newline at end of file +export const janApiClient = JanApiClient.getInstance() diff --git a/extensions-web/src/jan-provider-web/index.ts b/extensions-web/src/jan-provider-web/index.ts index 70cbf7770..4d3a4008a 100644 --- a/extensions-web/src/jan-provider-web/index.ts +++ b/extensions-web/src/jan-provider-web/index.ts @@ -1 +1 @@ -export { default } from './provider' \ No newline at end of file +export { default } from './provider' diff --git a/extensions-web/src/jan-provider-web/provider.ts b/extensions-web/src/jan-provider-web/provider.ts index cfbe18e2e..3375fd351 100644 --- a/extensions-web/src/jan-provider-web/provider.ts +++ b/extensions-web/src/jan-provider-web/provider.ts @@ -15,6 +15,7 @@ import { } from '@janhq/core' // cspell: disable-line import { janApiClient, JanChatMessage } from './api' import { janProviderStore } from './store' +import { ApiError } from '../shared/types/errors' // Jan models support tools via MCP const JAN_MODEL_CAPABILITIES = ['tools'] as const @@ -192,7 +193,8 @@ export default class JanProviderWeb extends AIEngine { console.error(`Failed to unload Jan session ${sessionId}:`, error) return { success: false, - error: error instanceof Error ? error.message : 'Unknown error', + error: error instanceof ApiError ? error.message : + error instanceof Error ? error.message : 'Unknown error', } } } diff --git a/extensions-web/src/jan-provider-web/store.ts b/extensions-web/src/jan-provider-web/store.ts index 02cc70686..2ff341147 100644 --- a/extensions-web/src/jan-provider-web/store.ts +++ b/extensions-web/src/jan-provider-web/store.ts @@ -92,4 +92,4 @@ export const janProviderStore = { useJanProviderStore.getState().clearError(), reset: () => useJanProviderStore.getState().reset(), -} \ No newline at end of file +} diff --git a/extensions-web/src/mcp-web/components/WebSearchButton.tsx b/extensions-web/src/mcp-web/components/WebSearchButton.tsx new file mode 100644 index 000000000..86fa08906 --- /dev/null +++ b/extensions-web/src/mcp-web/components/WebSearchButton.tsx @@ -0,0 +1,54 @@ +import { useMemo, useCallback } from 'react' +import { IconWorld } from '@tabler/icons-react' +import { MCPToolComponentProps } from '@janhq/core' + +// List of tool names considered as web search tools +const WEB_SEARCH_TOOL_NAMES = ['google_search', 'scrape']; + +export const WebSearchButton = ({ + tools, + isToolEnabled, + onToolToggle, +}: MCPToolComponentProps) => { + const webSearchTools = useMemo( + () => tools.filter((tool) => WEB_SEARCH_TOOL_NAMES.includes(tool.name)), + [tools] + ) + + // Early return if no web search tools available + if (webSearchTools.length === 0) { + return null + } + + // Check if all web search tools are enabled + const isEnabled = useMemo( + () => webSearchTools.every((tool) => isToolEnabled(tool.name)), + [webSearchTools, isToolEnabled] + ) + + const handleToggle = useCallback(() => { + // Toggle all web search tools at once + const newState = !isEnabled + webSearchTools.forEach((tool) => { + onToolToggle(tool.name, newState) + }) + }, [isEnabled, webSearchTools, onToolToggle]) + + return ( + + ) +} diff --git a/extensions-web/src/mcp-web/components/index.ts b/extensions-web/src/mcp-web/components/index.ts new file mode 100644 index 000000000..7f9bc47da --- /dev/null +++ b/extensions-web/src/mcp-web/components/index.ts @@ -0,0 +1 @@ +export { WebSearchButton } from './WebSearchButton' diff --git a/extensions-web/src/mcp-web/index.ts b/extensions-web/src/mcp-web/index.ts index 5e13846a7..705228c9d 100644 --- a/extensions-web/src/mcp-web/index.ts +++ b/extensions-web/src/mcp-web/index.ts @@ -4,11 +4,13 @@ * Uses official MCP TypeScript SDK with proper session handling */ -import { MCPExtension, MCPTool, MCPToolCallResult } from '@janhq/core' +import { MCPExtension, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '@janhq/core' import { getSharedAuthService, JanAuthService } from '../shared' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { JanMCPOAuthProvider } from './oauth-provider' +import { WebSearchButton } from './components' +import type { ComponentType } from 'react' // JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1') declare const JAN_API_BASE: string @@ -232,4 +234,12 @@ export default class MCPExtensionWeb extends MCPExtension { throw error } } -} \ No newline at end of file + + /** + * Provides a custom UI component for web search tools + * @returns The WebSearchButton component + */ + getToolComponent(): ComponentType | null { + return WebSearchButton + } +} diff --git a/extensions-web/src/mcp-web/oauth-provider.ts b/extensions-web/src/mcp-web/oauth-provider.ts index fd37c3ece..7d14264d6 100644 --- a/extensions-web/src/mcp-web/oauth-provider.ts +++ b/extensions-web/src/mcp-web/oauth-provider.ts @@ -57,4 +57,4 @@ export class JanMCPOAuthProvider implements OAuthClientProvider { async codeVerifier(): Promise { throw new Error('Code verifier not supported') } -} \ No newline at end of file +} diff --git a/extensions-web/src/shared/auth/service.ts b/extensions-web/src/shared/auth/service.ts index 1895ff8c4..eb15c4893 100644 --- a/extensions-web/src/shared/auth/service.ts +++ b/extensions-web/src/shared/auth/service.ts @@ -16,6 +16,7 @@ import { logoutUser, refreshToken, guestLogin } from './api' import { AuthProviderRegistry } from './registry' import { AuthBroadcast } from './broadcast' import type { ProviderType } from './providers' +import { ApiError } from '../types/errors' const authProviderRegistry = new AuthProviderRegistry() @@ -160,7 +161,7 @@ export class JanAuthService { this.tokenExpiryTime = Date.now() + tokens.expires_in * 1000 } catch (error) { console.error('Failed to refresh access token:', error) - if (error instanceof Error && error.message.includes('401')) { + if (error instanceof ApiError && error.isStatus(401)) { await this.handleSessionExpired() } throw error @@ -305,9 +306,7 @@ export class JanAuthService { if (!response.ok) { const errorText = await response.text() - throw new Error( - `API request failed: ${response.status} ${response.statusText} - ${errorText}` - ) + throw new ApiError(response.status, response.statusText, errorText) } return response.json() @@ -418,7 +417,7 @@ export class JanAuthService { ) } catch (error) { console.error('Failed to fetch user profile:', error) - if (error instanceof Error && error.message.includes('401')) { + if (error instanceof ApiError && error.isStatus(401)) { // Authentication failed - handle session expiry await this.handleSessionExpired() return null diff --git a/extensions-web/src/shared/types/errors.ts b/extensions-web/src/shared/types/errors.ts new file mode 100644 index 000000000..650507a7b --- /dev/null +++ b/extensions-web/src/shared/types/errors.ts @@ -0,0 +1,50 @@ +/** + * Shared error types for API responses + */ + +export class ApiError extends Error { + public readonly status: number + public readonly statusText: string + public readonly responseText: string + + constructor(status: number, statusText: string, responseText: string, message?: string) { + super(message || `API request failed: ${status} ${statusText} - ${responseText}`) + this.name = 'ApiError' + this.status = status + this.statusText = statusText + this.responseText = responseText + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if ((Error as any).captureStackTrace) { + (Error as any).captureStackTrace(this, ApiError) + } + } + + /** + * Check if this is a specific HTTP status code + */ + isStatus(code: number): boolean { + return this.status === code + } + + /** + * Check if this is a 404 Not Found error + */ + isNotFound(): boolean { + return this.status === 404 + } + + /** + * Check if this is a client error (4xx) + */ + isClientError(): boolean { + return this.status >= 400 && this.status < 500 + } + + /** + * Check if this is a server error (5xx) + */ + isServerError(): boolean { + return this.status >= 500 && this.status < 600 + } +} diff --git a/extensions-web/src/types.ts b/extensions-web/src/types.ts index 47ef0be71..4d52032c9 100644 --- a/extensions-web/src/types.ts +++ b/extensions-web/src/types.ts @@ -38,4 +38,4 @@ export interface IndexedDBConfig { keyPath: string indexes?: { name: string; keyPath: string | string[]; unique?: boolean }[] }[] -} \ No newline at end of file +} diff --git a/extensions-web/src/types/global.d.ts b/extensions-web/src/types/global.d.ts index a6e82d759..8d70d398b 100644 --- a/extensions-web/src/types/global.d.ts +++ b/extensions-web/src/types/global.d.ts @@ -2,4 +2,4 @@ export {} declare global { declare const JAN_API_BASE: string -} \ No newline at end of file +} diff --git a/extensions-web/src/vite-env.d.ts b/extensions-web/src/vite-env.d.ts index 151aa6856..11f02fe2a 100644 --- a/extensions-web/src/vite-env.d.ts +++ b/extensions-web/src/vite-env.d.ts @@ -1 +1 @@ -/// \ No newline at end of file +/// diff --git a/extensions-web/tsconfig.json b/extensions-web/tsconfig.json index e90dd4997..b39b50ee5 100644 --- a/extensions-web/tsconfig.json +++ b/extensions-web/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", + "jsx": "react-jsx", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "strict": true, diff --git a/extensions-web/vite.config.ts b/extensions-web/vite.config.ts index 89cfb7d0e..8c144b0ab 100644 --- a/extensions-web/vite.config.ts +++ b/extensions-web/vite.config.ts @@ -9,11 +9,11 @@ export default defineConfig({ fileName: 'index' }, rollupOptions: { - external: ['@janhq/core', 'zustand'] + external: ['@janhq/core', 'zustand', 'react', 'react-dom', 'react/jsx-runtime', '@tabler/icons-react'] }, emptyOutDir: false // Don't clean the output directory }, define: { JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'), } -}) \ No newline at end of file +}) diff --git a/package.json b/package.json index 50eb8ecaf..fe5fdff21 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,10 @@ "build:serve:web-app": "yarn build:web-app && yarn serve:web-app", "dev:tauri": "yarn build:icon && yarn copy:assets:tauri && cross-env IS_CLEAN=true tauri dev", "copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"", - "download:lib": "node ./scripts/download-lib.mjs", "download:bin": "node ./scripts/download-bin.mjs", "download:windows-installer": "node ./scripts/download-win-installer-deps.mjs", - "build:tauri:win32": "yarn download:bin && yarn download:lib && yarn download:windows-installer && yarn tauri build", - "build:tauri:linux": "yarn download:bin && yarn download:lib && NO_STRIP=1 ./src-tauri/build-utils/shim-linuxdeploy.sh yarn tauri build && ./src-tauri/build-utils/buildAppImage.sh", + "build:tauri:win32": "yarn download:bin && yarn download:windows-installer && yarn tauri build", + "build:tauri:linux": "yarn download:bin && NO_STRIP=1 ./src-tauri/build-utils/shim-linuxdeploy.sh yarn tauri build && ./src-tauri/build-utils/buildAppImage.sh", "build:tauri:darwin": "yarn download:bin && yarn tauri build --target universal-apple-darwin", "build:tauri": "yarn build:icon && yarn copy:assets:tauri && run-script-os", "build:tauri:plugin:api": "cd src-tauri/plugins && yarn install && yarn workspaces foreach -Apt run build", diff --git a/scripts/download-bin.mjs b/scripts/download-bin.mjs index 36e17b3f0..39eb9ae87 100644 --- a/scripts/download-bin.mjs +++ b/scripts/download-bin.mjs @@ -1,4 +1,3 @@ -console.log('Script is running') // scripts/download.js import https from 'https' import fs, { copyFile, mkdirSync } from 'fs' @@ -69,7 +68,10 @@ function getPlatformArch() { arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin' } else if (platform === 'linux') { bunPlatform = arch === 'arm64' ? 'linux-aarch64' : 'linux-x64' - uvPlatform = arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu' + uvPlatform = + arch === 'arm64' + ? 'aarch64-unknown-linux-gnu' + : 'x86_64-unknown-linux-gnu' } else if (platform === 'win32') { bunPlatform = 'windows-x64' // Bun has limited Windows support uvPlatform = 'x86_64-pc-windows-msvc' @@ -81,6 +83,10 @@ function getPlatformArch() { } async function main() { + if (process.env.SKIP_BINARIES) { + console.log('Skipping binaries download.') + process.exit(0) + } console.log('Starting main function') const platform = os.platform() const { bunPlatform, uvPlatform } = getPlatformArch() @@ -124,29 +130,45 @@ async function main() { if (err) { console.log('Add execution permission failed!', err) } - }); + }) if (platform === 'darwin') { - copyFile(path.join(binDir, 'bun'), path.join(binDir, 'bun-x86_64-apple-darwin'), (err) => { - if (err) { - console.log("Error Found:", err); - } - }) - copyFile(path.join(binDir, 'bun'), path.join(binDir, 'bun-aarch64-apple-darwin'), (err) => { - if (err) { - console.log("Error Found:", err); - } - }) - copyFile(path.join(binDir, 'bun'), path.join(binDir, 'bun-universal-apple-darwin'), (err) => { + copyFile( + path.join(binDir, 'bun'), + path.join(binDir, 'bun-x86_64-apple-darwin'), + (err) => { if (err) { - console.log("Error Found:", err); + console.log('Error Found:', err) } - }) - } else if (platform === 'linux') { - copyFile(path.join(binDir, 'bun'), path.join(binDir, 'bun-x86_64-unknown-linux-gnu'), (err) => { - if (err) { - console.log("Error Found:", err); } - }) + ) + copyFile( + path.join(binDir, 'bun'), + path.join(binDir, 'bun-aarch64-apple-darwin'), + (err) => { + if (err) { + console.log('Error Found:', err) + } + } + ) + copyFile( + path.join(binDir, 'bun'), + path.join(binDir, 'bun-universal-apple-darwin'), + (err) => { + if (err) { + console.log('Error Found:', err) + } + } + ) + } else if (platform === 'linux') { + copyFile( + path.join(binDir, 'bun'), + path.join(binDir, 'bun-x86_64-unknown-linux-gnu'), + (err) => { + if (err) { + console.log('Error Found:', err) + } + } + ) } } catch (err) { // Expect EEXIST error @@ -157,11 +179,15 @@ async function main() { path.join(binDir) ) if (platform === 'win32') { - copyFile(path.join(binDir, 'bun.exe'), path.join(binDir, 'bun-x86_64-pc-windows-msvc.exe'), (err) => { - if (err) { - console.log("Error Found:", err); + copyFile( + path.join(binDir, 'bun.exe'), + path.join(binDir, 'bun-x86_64-pc-windows-msvc.exe'), + (err) => { + if (err) { + console.log('Error Found:', err) + } } - }) + ) } } catch (err) { // Expect EEXIST error @@ -176,52 +202,66 @@ async function main() { await decompress(uvPath, tempBinDir) } try { - copySync( - path.join(tempBinDir, `uv-${uvPlatform}`, 'uv'), - path.join(binDir) - ) + copySync(path.join(tempBinDir, `uv-${uvPlatform}`, 'uv'), path.join(binDir)) fs.chmod(path.join(binDir, 'uv'), 0o755, (err) => { if (err) { console.log('Add execution permission failed!', err) } - }); + }) if (platform === 'darwin') { - copyFile(path.join(binDir, 'uv'), path.join(binDir, 'uv-x86_64-apple-darwin'), (err) => { - if (err) { - console.log("Error Found:", err); + copyFile( + path.join(binDir, 'uv'), + path.join(binDir, 'uv-x86_64-apple-darwin'), + (err) => { + if (err) { + console.log('Error Found:', err) + } } - }) - copyFile(path.join(binDir, 'uv'), path.join(binDir, 'uv-aarch64-apple-darwin'), (err) => { - if (err) { - console.log("Error Found:", err); + ) + copyFile( + path.join(binDir, 'uv'), + path.join(binDir, 'uv-aarch64-apple-darwin'), + (err) => { + if (err) { + console.log('Error Found:', err) + } } - }) - copyFile(path.join(binDir, 'uv'), path.join(binDir, 'uv-universal-apple-darwin'), (err) => { - if (err) { - console.log("Error Found:", err); + ) + copyFile( + path.join(binDir, 'uv'), + path.join(binDir, 'uv-universal-apple-darwin'), + (err) => { + if (err) { + console.log('Error Found:', err) + } } - }) + ) } else if (platform === 'linux') { - copyFile(path.join(binDir, 'uv'), path.join(binDir, 'uv-x86_64-unknown-linux-gnu'), (err) => { - if (err) { - console.log("Error Found:", err); + copyFile( + path.join(binDir, 'uv'), + path.join(binDir, 'uv-x86_64-unknown-linux-gnu'), + (err) => { + if (err) { + console.log('Error Found:', err) + } } - }) + ) } } catch (err) { // Expect EEXIST error } try { - copySync( - path.join(tempBinDir, 'uv.exe'), - path.join(binDir) - ) + copySync(path.join(tempBinDir, 'uv.exe'), path.join(binDir)) if (platform === 'win32') { - copyFile(path.join(binDir, 'uv.exe'), path.join(binDir, 'uv-x86_64-pc-windows-msvc.exe'), (err) => { - if (err) { - console.log("Error Found:", err); + copyFile( + path.join(binDir, 'uv.exe'), + path.join(binDir, 'uv-x86_64-pc-windows-msvc.exe'), + (err) => { + if (err) { + console.log('Error Found:', err) + } } - }) + ) } } catch (err) { // Expect EEXIST error diff --git a/scripts/download-lib.mjs b/scripts/download-lib.mjs deleted file mode 100644 index d2086b36e..000000000 --- a/scripts/download-lib.mjs +++ /dev/null @@ -1,86 +0,0 @@ -console.log('Script is running') -// scripts/download-lib.mjs -import https from 'https' -import fs, { mkdirSync } from 'fs' -import os from 'os' -import path from 'path' -import { copySync } from 'cpx' - -function download(url, dest) { - return new Promise((resolve, reject) => { - console.log(`Downloading ${url} to ${dest}`) - const file = fs.createWriteStream(dest) - https - .get(url, (response) => { - console.log(`Response status code: ${response.statusCode}`) - if ( - response.statusCode >= 300 && - response.statusCode < 400 && - response.headers.location - ) { - // Handle redirect - const redirectURL = response.headers.location - console.log(`Redirecting to ${redirectURL}`) - download(redirectURL, dest).then(resolve, reject) // Recursive call - return - } else if (response.statusCode !== 200) { - reject(`Failed to get '${url}' (${response.statusCode})`) - return - } - response.pipe(file) - file.on('finish', () => { - file.close(resolve) - }) - }) - .on('error', (err) => { - fs.unlink(dest, () => reject(err.message)) - }) - }) -} - -async function main() { - console.log('Starting main function') - const platform = os.platform() // 'darwin', 'linux', 'win32' - const arch = os.arch() // 'x64', 'arm64', etc. - - if (arch != 'x64') return - - let filename - if (platform == 'linux') - filename = 'libvulkan.so' - else if (platform == 'win32') - filename = 'vulkan-1.dll' - else - return - - const url = `https://catalog.jan.ai/${filename}` - - const libDir = 'src-tauri/resources/lib' - const tempDir = 'scripts/dist' - - try { - mkdirSync('scripts/dist') - } catch (err) { - // Expect EEXIST error if the directory already exists - } - - console.log(`Downloading libvulkan...`) - const savePath = path.join(tempDir, filename) - if (!fs.existsSync(savePath)) { - await download(url, savePath) - } - - // copy to tauri resources - try { - copySync(savePath, libDir) - } catch (err) { - // Expect EEXIST error - } - - console.log('Downloads completed.') -} - -main().catch((err) => { - console.error('Error:', err) - process.exit(1) -}) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 85a90422a..ca75cbd77 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5323,6 +5323,7 @@ dependencies = [ "sysinfo", "tauri", "tauri-plugin", + "tauri-plugin-hardware", "thiserror 2.0.12", "tokio", ] diff --git a/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml b/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml index eb74d32d1..7475fb353 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml +++ b/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml @@ -11,7 +11,7 @@ exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"] links = "tauri-plugin-hardware" [dependencies] -ash = "0.38.0" +vulkano = "0.34" libc = "0.2" log = "0.4" nvml-wrapper = "0.10.0" diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/commands.rs b/src-tauri/plugins/tauri-plugin-hardware/src/commands.rs index 56e78f1c1..ac13eb7f2 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/commands.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/commands.rs @@ -1,14 +1,12 @@ use crate::{ - helpers::get_jan_libvulkan_path, types::{CpuStaticInfo, SystemInfo, SystemUsage}, vendor::{nvidia, vulkan}, SYSTEM_INFO, }; use sysinfo::System; -use tauri::Runtime; #[tauri::command] -pub fn get_system_info(app: tauri::AppHandle) -> SystemInfo { +pub fn get_system_info() -> SystemInfo { SYSTEM_INFO .get_or_init(|| { let mut system = System::new(); @@ -19,15 +17,7 @@ pub fn get_system_info(app: tauri::AppHandle) -> SystemInfo { gpu_map.insert(gpu.uuid.clone(), gpu); } - // try system vulkan first - let paths = vec!["".to_string(), get_jan_libvulkan_path(app.clone())]; - let mut vulkan_gpus = vec![]; - for path in paths { - vulkan_gpus = vulkan::get_vulkan_gpus(&path); - if !vulkan_gpus.is_empty() { - break; - } - } + let vulkan_gpus = vulkan::get_vulkan_gpus(); for gpu in vulkan_gpus { match gpu_map.get_mut(&gpu.uuid) { @@ -64,7 +54,7 @@ pub fn get_system_info(app: tauri::AppHandle) -> SystemInfo { } #[tauri::command] -pub fn get_system_usage(app: tauri::AppHandle) -> SystemUsage { +pub fn get_system_usage() -> SystemUsage { let mut system = System::new(); system.refresh_memory(); @@ -81,7 +71,7 @@ pub fn get_system_usage(app: tauri::AppHandle) -> SystemUsage { cpu: cpu_usage, used_memory: system.used_memory() / 1024 / 1024, // bytes to MiB, total_memory: system.total_memory() / 1024 / 1024, // bytes to MiB, - gpus: get_system_info(app.clone()) + gpus: get_system_info() .gpus .iter() .map(|gpu| gpu.get_usage()) diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/helpers.rs b/src-tauri/plugins/tauri-plugin-hardware/src/helpers.rs deleted file mode 100644 index 22bcc8669..000000000 --- a/src-tauri/plugins/tauri-plugin-hardware/src/helpers.rs +++ /dev/null @@ -1,20 +0,0 @@ -use tauri::{path::BaseDirectory, Manager, Runtime}; - -pub fn get_jan_libvulkan_path(app: tauri::AppHandle) -> String { - let lib_name = if cfg!(target_os = "windows") { - "vulkan-1.dll" - } else if cfg!(target_os = "linux") { - "libvulkan.so" - } else { - return "".to_string(); - }; - - // NOTE: this does not work in test mode (mock app) - match app.path().resolve( - format!("resources/lib/{}", lib_name), - BaseDirectory::Resource, - ) { - Ok(lib_path) => lib_path.to_string_lossy().to_string(), - Err(_) => "".to_string(), - } -} diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/lib.rs b/src-tauri/plugins/tauri-plugin-hardware/src/lib.rs index 228a3731e..3a069892e 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/lib.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/lib.rs @@ -2,12 +2,10 @@ mod commands; mod constants; pub mod cpu; pub mod gpu; -mod helpers; mod types; pub mod vendor; pub use constants::*; -pub use helpers::*; pub use types::*; use std::sync::OnceLock; diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/tests.rs b/src-tauri/plugins/tauri-plugin-hardware/src/tests.rs index 1d4975104..f27554579 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/tests.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/tests.rs @@ -4,15 +4,13 @@ use tauri::test::mock_app; #[test] fn test_system_info() { - let app = mock_app(); - let info = get_system_info(app.handle().clone()); + let info = get_system_info(); println!("System Static Info: {:?}", info); } #[test] fn test_system_usage() { - let app = mock_app(); - let usage = get_system_usage(app.handle().clone()); + let usage = get_system_usage(); println!("System Usage Info: {:?}", usage); } @@ -23,23 +21,23 @@ mod cpu_tests { #[test] fn test_cpu_static_info_new() { let cpu_info = CpuStaticInfo::new(); - + // Test that all fields are populated assert!(!cpu_info.name.is_empty()); assert_ne!(cpu_info.name, "unknown"); // Should have detected a CPU name assert!(cpu_info.core_count > 0); assert!(!cpu_info.arch.is_empty()); - + // Architecture should be one of the expected values assert!( - cpu_info.arch == "aarch64" || - cpu_info.arch == "arm64" || - cpu_info.arch == "x86_64" || - cpu_info.arch == std::env::consts::ARCH + cpu_info.arch == "aarch64" + || cpu_info.arch == "arm64" + || cpu_info.arch == "x86_64" + || cpu_info.arch == std::env::consts::ARCH ); - + // Extensions should be a valid list (can be empty on non-x86) - + println!("CPU Info: {:?}", cpu_info); } @@ -48,7 +46,7 @@ mod cpu_tests { // Test that multiple calls return consistent information let info1 = CpuStaticInfo::new(); let info2 = CpuStaticInfo::new(); - + assert_eq!(info1.name, info2.name); assert_eq!(info1.core_count, info2.core_count); assert_eq!(info1.arch, info2.arch); @@ -72,19 +70,41 @@ mod cpu_tests { #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn test_x86_extensions() { let cpu_info = CpuStaticInfo::new(); - + // On x86/x86_64, we should always have at least FPU assert!(cpu_info.extensions.contains(&"fpu".to_string())); - + // Check that all extensions are valid x86 feature names let valid_extensions = [ - "fpu", "mmx", "sse", "sse2", "sse3", "ssse3", "sse4_1", "sse4_2", - "pclmulqdq", "avx", "avx2", "avx512_f", "avx512_dq", "avx512_ifma", - "avx512_pf", "avx512_er", "avx512_cd", "avx512_bw", "avx512_vl", - "avx512_vbmi", "avx512_vbmi2", "avx512_vnni", "avx512_bitalg", - "avx512_vpopcntdq", "avx512_vp2intersect", "aes", "f16c" + "fpu", + "mmx", + "sse", + "sse2", + "sse3", + "ssse3", + "sse4_1", + "sse4_2", + "pclmulqdq", + "avx", + "avx2", + "avx512_f", + "avx512_dq", + "avx512_ifma", + "avx512_pf", + "avx512_er", + "avx512_cd", + "avx512_bw", + "avx512_vl", + "avx512_vbmi", + "avx512_vbmi2", + "avx512_vnni", + "avx512_bitalg", + "avx512_vpopcntdq", + "avx512_vp2intersect", + "aes", + "f16c", ]; - + for ext in &cpu_info.extensions { assert!( valid_extensions.contains(&ext.as_str()), @@ -98,7 +118,7 @@ mod cpu_tests { #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] fn test_non_x86_extensions() { let cpu_info = CpuStaticInfo::new(); - + // On non-x86 architectures, extensions should be empty assert!(cpu_info.extensions.is_empty()); } @@ -106,15 +126,15 @@ mod cpu_tests { #[test] fn test_arch_detection() { let cpu_info = CpuStaticInfo::new(); - + // Architecture should be a valid string assert!(!cpu_info.arch.is_empty()); - + // Should be one of the common architectures let common_archs = ["x86_64", "aarch64", "arm", "arm64", "x86"]; let is_common_arch = common_archs.iter().any(|&arch| cpu_info.arch == arch); let is_compile_time_arch = cpu_info.arch == std::env::consts::ARCH; - + assert!( is_common_arch || is_compile_time_arch, "Unexpected architecture: {}", @@ -125,11 +145,11 @@ mod cpu_tests { #[test] fn test_cpu_info_serialization() { let cpu_info = CpuStaticInfo::new(); - + // Test that the struct can be serialized (since it derives Serialize) let serialized = serde_json::to_string(&cpu_info); assert!(serialized.is_ok()); - + let json_str = serialized.unwrap(); assert!(json_str.contains("name")); assert!(json_str.contains("core_count")); diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs index 078efe91b..ad48a6fad 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs @@ -12,7 +12,7 @@ fn test_get_nvidia_gpus() { #[test] fn test_get_vulkan_gpus() { - let gpus = vulkan::get_vulkan_gpus(""); + let gpus = vulkan::get_vulkan_gpus(); for (i, gpu) in gpus.iter().enumerate() { println!("GPU {}:", i); println!(" {:?}", gpu); diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs index 6a9bf21aa..91af52e5f 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs @@ -1,5 +1,8 @@ use crate::types::{GpuInfo, Vendor}; -use ash::{vk, Entry}; +use vulkano::device::physical::PhysicalDeviceType; +use vulkano::instance::{Instance, InstanceCreateInfo}; +use vulkano::memory::MemoryHeapFlags; +use vulkano::VulkanLibrary; #[derive(Debug, Clone, serde::Serialize)] pub struct VulkanInfo { @@ -35,8 +38,8 @@ fn parse_uuid(bytes: &[u8; 16]) -> String { ) } -pub fn get_vulkan_gpus(lib_path: &str) -> Vec { - match get_vulkan_gpus_internal(lib_path) { +pub fn get_vulkan_gpus() -> Vec { + match get_vulkan_gpus_internal() { Ok(gpus) => gpus, Err(e) => { log::error!("Failed to get Vulkan GPUs: {:?}", e); @@ -45,86 +48,59 @@ pub fn get_vulkan_gpus(lib_path: &str) -> Vec { } } -fn parse_c_string(buf: &[i8]) -> String { - unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) } - .to_str() - .unwrap_or_default() - .to_string() -} +fn get_vulkan_gpus_internal() -> Result, Box> { + let library = VulkanLibrary::new()?; -fn get_vulkan_gpus_internal(lib_path: &str) -> Result, Box> { - let entry = if lib_path.is_empty() { - unsafe { Entry::load()? } - } else { - unsafe { Entry::load_from(lib_path)? } - }; - let app_info = vk::ApplicationInfo { - api_version: vk::make_api_version(0, 1, 1, 0), - ..Default::default() - }; - let create_info = vk::InstanceCreateInfo { - p_application_info: &app_info, - ..Default::default() - }; - let instance = unsafe { entry.create_instance(&create_info, None)? }; + let instance = Instance::new( + library, + InstanceCreateInfo { + application_name: Some("Jan GPU Detection".into()), + application_version: vulkano::Version::V1_1, + ..Default::default() + }, + )?; let mut device_info_list = vec![]; - for (i, device) in unsafe { instance.enumerate_physical_devices()? } - .iter() - .enumerate() - { - // create a chain of properties struct for VkPhysicalDeviceProperties2(3) - // https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceProperties2.html - // props2 -> driver_props -> id_props - let mut id_props = vk::PhysicalDeviceIDProperties::default(); - let mut driver_props = vk::PhysicalDeviceDriverProperties { - p_next: &mut id_props as *mut _ as *mut std::ffi::c_void, - ..Default::default() - }; - let mut props2 = vk::PhysicalDeviceProperties2 { - p_next: &mut driver_props as *mut _ as *mut std::ffi::c_void, - ..Default::default() - }; - unsafe { - instance.get_physical_device_properties2(*device, &mut props2); - } + for (i, physical_device) in instance.enumerate_physical_devices()?.enumerate() { + let properties = physical_device.properties(); - let props = props2.properties; - if props.device_type == vk::PhysicalDeviceType::CPU { + if properties.device_type == PhysicalDeviceType::Cpu { continue; } + let memory_properties = physical_device.memory_properties(); + let total_memory: u64 = memory_properties + .memory_heaps + .iter() + .filter(|heap| heap.flags.intersects(MemoryHeapFlags::DEVICE_LOCAL)) + .map(|heap| heap.size / (1024 * 1024)) + .sum(); + + let device_uuid = physical_device.properties().device_uuid.unwrap_or([0; 16]); + let driver_version = format!("{}", properties.driver_version); + let device_info = GpuInfo { - name: parse_c_string(&props.device_name), - total_memory: unsafe { instance.get_physical_device_memory_properties(*device) } - .memory_heaps - .iter() - .filter(|heap| heap.flags.contains(vk::MemoryHeapFlags::DEVICE_LOCAL)) - .map(|heap| heap.size / (1024 * 1024)) - .sum(), - vendor: Vendor::from_vendor_id(props.vendor_id), - uuid: parse_uuid(&id_props.device_uuid), - driver_version: parse_c_string(&driver_props.driver_info), + name: properties.device_name.clone(), + total_memory, + vendor: Vendor::from_vendor_id(properties.vendor_id), + uuid: parse_uuid(&device_uuid), + driver_version, nvidia_info: None, vulkan_info: Some(VulkanInfo { index: i as u64, - device_type: format!("{:?}", props.device_type), + device_type: format!("{:?}", properties.device_type), api_version: format!( "{}.{}.{}", - vk::api_version_major(props.api_version), - vk::api_version_minor(props.api_version), - vk::api_version_patch(props.api_version) + properties.api_version.major, + properties.api_version.minor, + properties.api_version.patch ), - device_id: props.device_id, + device_id: properties.device_id, }), }; device_info_list.push(device_info); } - unsafe { - instance.destroy_instance(None); - } - Ok(device_info_list) } diff --git a/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/commands.rs b/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/commands.rs index c636fa8bd..03e949eba 100644 --- a/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/commands.rs +++ b/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/commands.rs @@ -3,7 +3,6 @@ use super::utils::{estimate_kv_cache_internal, read_gguf_metadata_internal}; use crate::gguf::types::{KVCacheError, KVCacheEstimate, ModelSupportStatus}; use std::collections::HashMap; use std::fs; -use tauri::Runtime; use tauri_plugin_hardware::get_system_info; /// Read GGUF metadata from a model file #[tauri::command] @@ -49,16 +48,15 @@ pub async fn get_model_size(path: String) -> Result { } #[tauri::command] -pub async fn is_model_supported( +pub async fn is_model_supported( path: String, ctx_size: Option, - app_handle: tauri::AppHandle, ) -> Result { // Get model size let model_size = get_model_size(path.clone()).await?; // Get system info - let system_info = get_system_info(app_handle.clone()); + let system_info = get_system_info(); log::info!("modelSize: {}", model_size); diff --git a/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/model_planner.rs b/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/model_planner.rs index 118894871..14642af60 100644 --- a/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/model_planner.rs +++ b/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/model_planner.rs @@ -3,7 +3,6 @@ use crate::gguf::utils::estimate_kv_cache_internal; use crate::gguf::utils::read_gguf_metadata_internal; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use tauri::Runtime; use tauri_plugin_hardware::get_system_info; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -27,15 +26,14 @@ pub enum ModelMode { } #[tauri::command] -pub async fn plan_model_load( +pub async fn plan_model_load( path: String, memory_mode: String, mmproj_path: Option, requested_ctx: Option, - app: tauri::AppHandle, ) -> Result { let model_size = get_model_size(path.clone()).await?; - let sys_info = get_system_info(app.clone()); + let sys_info = get_system_info(); let gguf = read_gguf_metadata_internal(path.clone()).await?; let mut mmproj_size: u64 = 0; diff --git a/src-tauri/src/core/mcp/helpers.rs b/src-tauri/src/core/mcp/helpers.rs index 442ddcf12..9431de48b 100644 --- a/src-tauri/src/core/mcp/helpers.rs +++ b/src-tauri/src/core/mcp/helpers.rs @@ -25,7 +25,7 @@ use crate::core::{ mcp::models::McpServerConfig, state::{AppState, RunningServiceEnum, SharedMcpServers}, }; -use jan_utils::can_override_npx; +use jan_utils::{can_override_npx, can_override_uvx}; /// Calculate exponential backoff delay with jitter /// @@ -627,19 +627,20 @@ async fn schedule_mcp_start_task( } } else { let mut cmd = Command::new(config_params.command.clone()); - if config_params.command.clone() == "npx" && can_override_npx() { + let bun_x_path = format!("{}/bun", bin_path.display()); + if config_params.command.clone() == "npx" && can_override_npx(bun_x_path.clone()) { let mut cache_dir = app_path.clone(); cache_dir.push(".npx"); - let bun_x_path = format!("{}/bun", bin_path.display()); cmd = Command::new(bun_x_path); cmd.arg("x"); cmd.env("BUN_INSTALL", cache_dir.to_str().unwrap().to_string()); } - if config_params.command.clone() == "uvx" { + + let uv_path = format!("{}/uv", bin_path.display()); + if config_params.command.clone() == "uvx" && can_override_uvx(uv_path.clone()) { let mut cache_dir = app_path.clone(); cache_dir.push(".uvx"); - let bun_x_path = format!("{}/uv", bin_path.display()); - cmd = Command::new(bun_x_path); + cmd = Command::new(uv_path); cmd.arg("tool"); cmd.arg("run"); cmd.env("UV_CACHE_DIR", cache_dir.to_str().unwrap().to_string()); diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 80e7446ff..6b0684b25 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -6,13 +6,11 @@ "linux": { "appimage": { "bundleMediaFramework": false, - "files": { - } + "files": {} }, "deb": { "files": { - "usr/bin/bun": "resources/bin/bun", - "usr/lib/Jan/resources/lib/libvulkan.so": "resources/lib/libvulkan.so" + "usr/bin/bun": "resources/bin/bun" } } } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index f12ee2a5c..dad811467 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,7 +1,11 @@ { "bundle": { - "targets": ["nsis", "msi"], - "resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll", "resources/LICENSE", "resources/lib/vc_redist.x64.exe"], + "targets": ["nsis"], + "resources": [ + "resources/pre-install/**/*", + "resources/lib/vc_redist.x64.exe", + "resources/LICENSE" + ], "externalBin": ["resources/bin/bun", "resources/bin/uv"], "windows": { "nsis": { diff --git a/src-tauri/utils/src/system.rs b/src-tauri/utils/src/system.rs index d4ebc79af..cf281b3cb 100644 --- a/src-tauri/utils/src/system.rs +++ b/src-tauri/utils/src/system.rs @@ -1,5 +1,5 @@ -/// Checks AVX2 CPU support for npx override with bun binary -pub fn can_override_npx() -> bool { +/// Checks if npx can be overridden with bun binary +pub fn can_override_npx(bun_path: String) -> bool { // We need to check the CPU for the AVX2 instruction support if we are running under MacOS // with Intel CPU. We can override `npx` command with `bun` only if CPU is // supporting AVX2, otherwise we need to use default `npx` binary @@ -13,10 +13,31 @@ pub fn can_override_npx() -> bool { return false; // we cannot override npx with bun binary } } - + // Check if bun_path exists + if !std::path::Path::new(bun_path.as_str()).exists() { + #[cfg(feature = "logging")] + log::warn!( + "bun binary not found at '{}', default npx binary will be used", + bun_path + ); + return false; + } true // by default, we can override npx with bun binary } +/// Checks if uv_path exists and determines if uvx can be overridden with the uv binary +pub fn can_override_uvx(uv_path: String) -> bool { + if !std::path::Path::new(uv_path.as_str()).exists() { + #[cfg(feature = "logging")] + log::warn!( + "uv binary not found at '{}', default uvx binary will be used", + uv_path + ); + return false; + } + true // by default, we can override uvx with uv binary +} + /// Setup library paths for different operating systems pub fn setup_library_path(library_path: Option<&str>, command: &mut tokio::process::Command) { if let Some(lib_path) = library_path { diff --git a/web-app/package.json b/web-app/package.json index da7849f87..88bbe411a 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -21,8 +21,8 @@ "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", - "@jan/extensions-web": "link:../extensions-web", - "@janhq/core": "link:../core", + "@jan/extensions-web": "workspace:*", + "@janhq/core": "workspace:*", "@radix-ui/react-accordion": "1.2.11", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-dialog": "1.1.15", diff --git a/web-app/src/__tests__/i18n.test.ts b/web-app/src/__tests__/i18n.test.ts index 644bc019d..262d93194 100644 --- a/web-app/src/__tests__/i18n.test.ts +++ b/web-app/src/__tests__/i18n.test.ts @@ -49,4 +49,4 @@ describe('i18n module', () => { expect(i18nModule[exportName]).toBeDefined() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/__tests__/main.test.tsx b/web-app/src/__tests__/main.test.tsx index c105482bf..aec753d56 100644 --- a/web-app/src/__tests__/main.test.tsx +++ b/web-app/src/__tests__/main.test.tsx @@ -76,4 +76,4 @@ describe('main.tsx', () => { await import('../main') }).rejects.toThrow() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/dialog.test.tsx b/web-app/src/components/ui/__tests__/dialog.test.tsx index b4c1f5aab..aeb0cbf52 100644 --- a/web-app/src/components/ui/__tests__/dialog.test.tsx +++ b/web-app/src/components/ui/__tests__/dialog.test.tsx @@ -416,4 +416,4 @@ describe('Dialog Components', () => { expect(screen.getByText('Dialog description')).toHaveAttribute('data-slot', 'dialog-description') expect(screen.getByText('Footer button').closest('div')).toHaveAttribute('data-slot', 'dialog-footer') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx b/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx index 7b0da6f76..541ae0f93 100644 --- a/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx +++ b/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx @@ -853,4 +853,4 @@ describe('DropdownMenu Components', () => { expect(handleItemClick).toHaveBeenCalledTimes(1) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/dropdrawer.test.tsx b/web-app/src/components/ui/__tests__/dropdrawer.test.tsx index 6203d9f4e..cef88b9d9 100644 --- a/web-app/src/components/ui/__tests__/dropdrawer.test.tsx +++ b/web-app/src/components/ui/__tests__/dropdrawer.test.tsx @@ -530,4 +530,4 @@ describe('DropDrawer Component', () => { expect(trigger).toHaveAttribute('aria-haspopup', 'dialog') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/hover-card.test.tsx b/web-app/src/components/ui/__tests__/hover-card.test.tsx index 71e78cb7f..5e0a39628 100644 --- a/web-app/src/components/ui/__tests__/hover-card.test.tsx +++ b/web-app/src/components/ui/__tests__/hover-card.test.tsx @@ -165,4 +165,4 @@ describe('HoverCard Components', () => { expect(screen.getByText('Hover content')).toBeDefined() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/input.test.tsx b/web-app/src/components/ui/__tests__/input.test.tsx index 2ae18adad..ddf2fa7db 100644 --- a/web-app/src/components/ui/__tests__/input.test.tsx +++ b/web-app/src/components/ui/__tests__/input.test.tsx @@ -93,4 +93,4 @@ describe('Input', () => { fireEvent.blur(input) expect(handleBlur).toHaveBeenCalledTimes(1) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/popover.test.tsx b/web-app/src/components/ui/__tests__/popover.test.tsx index cec809bb7..b76d1ce04 100644 --- a/web-app/src/components/ui/__tests__/popover.test.tsx +++ b/web-app/src/components/ui/__tests__/popover.test.tsx @@ -436,4 +436,4 @@ describe('Popover Components', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/progress.test.tsx b/web-app/src/components/ui/__tests__/progress.test.tsx index daa4b5c05..90a7bc70f 100644 --- a/web-app/src/components/ui/__tests__/progress.test.tsx +++ b/web-app/src/components/ui/__tests__/progress.test.tsx @@ -84,4 +84,4 @@ describe('Progress', () => { // For values over 100, the transform should be positive expect(indicator?.style.transform).toContain('translateX(--50%)') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/radio-group.test.tsx b/web-app/src/components/ui/__tests__/radio-group.test.tsx index a788931d8..1cb85e7c6 100644 --- a/web-app/src/components/ui/__tests__/radio-group.test.tsx +++ b/web-app/src/components/ui/__tests__/radio-group.test.tsx @@ -59,4 +59,4 @@ describe('RadioGroup', () => { expect(screen.getByLabelText('HTTP')).toBeChecked() expect(screen.getByLabelText('SSE')).not.toBeChecked() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/sheet.test.tsx b/web-app/src/components/ui/__tests__/sheet.test.tsx index dc21bbe66..988e512c0 100644 --- a/web-app/src/components/ui/__tests__/sheet.test.tsx +++ b/web-app/src/components/ui/__tests__/sheet.test.tsx @@ -260,4 +260,4 @@ describe('Sheet Components', () => { expect(screen.getByText('Main Content')).toBeInTheDocument() expect(screen.getByText('Close')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/skeleton.test.tsx b/web-app/src/components/ui/__tests__/skeleton.test.tsx index 273be182e..39d9535a7 100644 --- a/web-app/src/components/ui/__tests__/skeleton.test.tsx +++ b/web-app/src/components/ui/__tests__/skeleton.test.tsx @@ -61,4 +61,4 @@ describe('Skeleton', () => { expect(skeleton).toHaveClass('w-full') expect(skeleton).toHaveClass('bg-red-500') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/slider.test.tsx b/web-app/src/components/ui/__tests__/slider.test.tsx index 5fd72f766..2a15441b2 100644 --- a/web-app/src/components/ui/__tests__/slider.test.tsx +++ b/web-app/src/components/ui/__tests__/slider.test.tsx @@ -190,4 +190,4 @@ describe('Slider', () => { expect(thumb).toHaveClass('border-accent', 'bg-main-view', 'rounded-full') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/sonner.test.tsx b/web-app/src/components/ui/__tests__/sonner.test.tsx index 72aca5526..8b2fc762d 100644 --- a/web-app/src/components/ui/__tests__/sonner.test.tsx +++ b/web-app/src/components/ui/__tests__/sonner.test.tsx @@ -90,4 +90,4 @@ describe('Toaster Component', () => { expect(toaster).toHaveAttribute('data-rich-colors', 'true') expect(toaster).toHaveAttribute('data-close-button', 'true') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/switch.test.tsx b/web-app/src/components/ui/__tests__/switch.test.tsx index d872dbc11..0db35d716 100644 --- a/web-app/src/components/ui/__tests__/switch.test.tsx +++ b/web-app/src/components/ui/__tests__/switch.test.tsx @@ -189,4 +189,4 @@ describe('Switch', () => { const switchElement = document.querySelector('[data-slot="switch"]') expect(switchElement).toHaveClass('data-[state=unchecked]:bg-main-view-fg/20') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/textarea.test.tsx b/web-app/src/components/ui/__tests__/textarea.test.tsx index 6daf09e4d..806cf9515 100644 --- a/web-app/src/components/ui/__tests__/textarea.test.tsx +++ b/web-app/src/components/ui/__tests__/textarea.test.tsx @@ -113,4 +113,4 @@ describe('Textarea', () => { const textarea = screen.getByRole('textbox') expect(textarea).toHaveAttribute('cols', '50') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/tooltip.test.tsx b/web-app/src/components/ui/__tests__/tooltip.test.tsx index 4221751d4..46c68e0b8 100644 --- a/web-app/src/components/ui/__tests__/tooltip.test.tsx +++ b/web-app/src/components/ui/__tests__/tooltip.test.tsx @@ -111,4 +111,4 @@ describe('Tooltip Components', () => { expect(screen.getByText('First')).toBeInTheDocument() expect(screen.getByText('Second')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/radio-group.tsx b/web-app/src/components/ui/radio-group.tsx index b5fa0593a..e604ac901 100644 --- a/web-app/src/components/ui/radio-group.tsx +++ b/web-app/src/components/ui/radio-group.tsx @@ -39,4 +39,4 @@ const RadioGroupItem = React.forwardRef< }) RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName -export { RadioGroup, RadioGroupItem } \ No newline at end of file +export { RadioGroup, RadioGroupItem } diff --git a/web-app/src/constants/__tests__/windows.test.ts b/web-app/src/constants/__tests__/windows.test.ts index f9d388f98..3f55184e1 100644 --- a/web-app/src/constants/__tests__/windows.test.ts +++ b/web-app/src/constants/__tests__/windows.test.ts @@ -33,4 +33,4 @@ describe('windows constants', () => { expect(value.length).toBeGreaterThan(0) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/constants/chat.ts b/web-app/src/constants/chat.ts new file mode 100644 index 000000000..e1649049f --- /dev/null +++ b/web-app/src/constants/chat.ts @@ -0,0 +1,6 @@ +/** + * Chat-related constants + */ + +export const TEMPORARY_CHAT_ID = 'temporary-chat' +export const TEMPORARY_CHAT_QUERY_ID = 'temporary-chat' diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index cba580ebd..6e314a0f5 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -38,6 +38,9 @@ import { useTools } from '@/hooks/useTools' import { TokenCounter } from '@/components/TokenCounter' import { useMessages } from '@/hooks/useMessages' import { useShallow } from 'zustand/react/shallow' +import { McpExtensionToolLoader } from './McpExtensionToolLoader' +import { ExtensionTypeEnum, MCPExtension } from '@janhq/core' +import { ExtensionManager } from '@/lib/extension' type ChatInputProps = { className?: string @@ -171,6 +174,12 @@ const ChatInput = ({ // Check if there are active MCP servers const hasActiveMCPServers = connectedServers.length > 0 || tools.length > 0 + // Get MCP extension and its custom component + const extensionManager = ExtensionManager.getInstance() + const mcpExtension = extensionManager.get(ExtensionTypeEnum.MCP) + const MCPToolComponent = mcpExtension?.getToolComponent?.() + + const handleSendMesage = (prompt: string) => { if (!selectedModel) { setMessage('Please select a model to start chatting.') @@ -719,60 +728,72 @@ const ChatInput = ({ {selectedModel?.capabilities?.includes('tools') && hasActiveMCPServers && ( - - - + ) : ( + // Use default tools dropdown + + -
    { - setDropdownToolsAvailable(false) - e.stopPropagation() - }} + - { - setDropdownToolsAvailable(isOpen) - if (isOpen) { - setTooltipToolsAvailable(false) - } +
    { + setDropdownToolsAvailable(false) + e.stopPropagation() }} > - {(isOpen, toolsCount) => { - return ( -
    - - {toolsCount > 0 && ( -
    - - {toolsCount > 99 ? '99+' : toolsCount} - -
    - )} -
    - ) - }} - -
    -
    - -

    {t('tools')}

    -
    - - + { + setDropdownToolsAvailable(isOpen) + if (isOpen) { + setTooltipToolsAvailable(false) + } + }} + > + {(isOpen, toolsCount) => { + return ( +
    + + {toolsCount > 0 && ( +
    + + {toolsCount > 99 ? '99+' : toolsCount} + +
    + )} +
    + ) + }} +
    +
    +
    + +

    {t('tools')}

    +
    +
    +
    + ) )} {selectedModel?.capabilities?.includes('web_search') && ( diff --git a/web-app/src/containers/DropdownModelProvider.tsx b/web-app/src/containers/DropdownModelProvider.tsx index 8f9ea35a8..a8614f89d 100644 --- a/web-app/src/containers/DropdownModelProvider.tsx +++ b/web-app/src/containers/DropdownModelProvider.tsx @@ -6,7 +6,7 @@ import { PopoverTrigger, } from '@/components/ui/popover' import { useModelProvider } from '@/hooks/useModelProvider' -import { cn, getProviderTitle } from '@/lib/utils' +import { cn, getProviderTitle, getModelDisplayName } from '@/lib/utils' import { highlightFzfMatch } from '@/utils/highlight' import Capabilities from './Capabilities' import { IconSettings, IconX } from '@tabler/icons-react' @@ -240,7 +240,7 @@ const DropdownModelProvider = ({ // Update display model when selection changes useEffect(() => { if (selectedProvider && selectedModel) { - setDisplayModel(selectedModel.id) + setDisplayModel(getModelDisplayName(selectedModel)) } else { setDisplayModel(t('common:selectAModel')) } @@ -326,7 +326,7 @@ const DropdownModelProvider = ({ // Create Fzf instance for fuzzy search const fzfInstance = useMemo(() => { return new Fzf(searchableItems, { - selector: (item) => item.model.id.toLowerCase(), + selector: (item) => `${getModelDisplayName(item.model)} ${item.model.id}`.toLowerCase(), }) }, [searchableItems]) @@ -390,7 +390,7 @@ const DropdownModelProvider = ({ const handleSelect = useCallback( async (searchableModel: SearchableModel) => { // Immediately update display to prevent double-click issues - setDisplayModel(searchableModel.model.id) + setDisplayModel(getModelDisplayName(searchableModel.model)) setSearchValue('') setOpen(false) @@ -576,7 +576,7 @@ const DropdownModelProvider = ({ /> - {searchableModel.model.id} + {getModelDisplayName(searchableModel.model)}
    {capabilities.length > 0 && ( @@ -669,7 +669,7 @@ const DropdownModelProvider = ({ className="text-main-view-fg/80 text-sm" title={searchableModel.model.id} > - {searchableModel.model.id} + {getModelDisplayName(searchableModel.model)}
    {capabilities.length > 0 && ( diff --git a/web-app/src/containers/HeaderPage.tsx b/web-app/src/containers/HeaderPage.tsx index 7c47e9273..ffa9b0aa2 100644 --- a/web-app/src/containers/HeaderPage.tsx +++ b/web-app/src/containers/HeaderPage.tsx @@ -1,13 +1,40 @@ import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' -import { IconLayoutSidebar } from '@tabler/icons-react' +import { IconLayoutSidebar, IconMessage, IconMessageFilled } from '@tabler/icons-react' import { ReactNode } from '@tanstack/react-router' +import { useRouter } from '@tanstack/react-router' +import { route } from '@/constants/routes' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' +import { TEMPORARY_CHAT_QUERY_ID } from '@/constants/chat' type HeaderPageProps = { children?: ReactNode } const HeaderPage = ({ children }: HeaderPageProps) => { const { open, setLeftPanel } = useLeftPanel() + const router = useRouter() + const currentPath = router.state.location.pathname + + const isHomePage = currentPath === route.home + + // Parse temporary chat flag from URL search params directly to avoid invariant errors + const searchString = window.location.search + const urlSearchParams = new URLSearchParams(searchString) + const isTemporaryChat = isHomePage && urlSearchParams.get(TEMPORARY_CHAT_QUERY_ID) === 'true' + + const handleChatToggle = () => { + console.log('Chat toggle clicked!', { isTemporaryChat, isHomePage, currentPath }) + if (isHomePage) { + if (isTemporaryChat) { + console.log('Switching to regular chat') + router.navigate({ to: route.home, search: {} }) + } else { + console.log('Switching to temporary chat') + router.navigate({ to: route.home, search: { [TEMPORARY_CHAT_QUERY_ID]: true } }) + } + } + } return (
    { )} {children} + + {/* Temporary Chat Toggle - Only show on home page if feature is enabled */} + {PlatformFeatures[PlatformFeature.TEMPORARY_CHAT] && isHomePage && ( +
    + +
    + )}
    ) diff --git a/web-app/src/containers/McpExtensionToolLoader.tsx b/web-app/src/containers/McpExtensionToolLoader.tsx new file mode 100644 index 000000000..ccadada6c --- /dev/null +++ b/web-app/src/containers/McpExtensionToolLoader.tsx @@ -0,0 +1,61 @@ +import { ComponentType } from 'react' +import { MCPTool, MCPToolComponentProps } from '@janhq/core' +import { useToolAvailable } from '@/hooks/useToolAvailable' +import { useThreads } from '@/hooks/useThreads' + +interface McpExtensionToolLoaderProps { + tools: MCPTool[] + hasActiveMCPServers: boolean + selectedModelHasTools: boolean + initialMessage?: boolean + MCPToolComponent?: ComponentType | null +} + +export const McpExtensionToolLoader = ({ + tools, + hasActiveMCPServers, + selectedModelHasTools, + initialMessage, + MCPToolComponent, +}: McpExtensionToolLoaderProps) => { + // Get tool management hooks + const { isToolDisabled, setToolDisabledForThread, setDefaultDisabledTools, getDefaultDisabledTools } = useToolAvailable() + const { getCurrentThread } = useThreads() + const currentThread = getCurrentThread() + + // Handle tool toggle for custom component + const handleToolToggle = (toolName: string, enabled: boolean) => { + if (initialMessage) { + const currentDefaults = getDefaultDisabledTools() + if (enabled) { + setDefaultDisabledTools(currentDefaults.filter((name) => name !== toolName)) + } else { + setDefaultDisabledTools([...currentDefaults, toolName]) + } + } else if (currentThread?.id) { + setToolDisabledForThread(currentThread.id, toolName, enabled) + } + } + + const isToolEnabled = (toolName: string): boolean => { + if (initialMessage) { + return !getDefaultDisabledTools().includes(toolName) + } else if (currentThread?.id) { + return !isToolDisabled(currentThread.id, toolName) + } + return false + } + + // Only render if we have the custom MCP component and conditions are met + if (!selectedModelHasTools || !hasActiveMCPServers || !MCPToolComponent) { + return null + } + + return ( + + ) +} diff --git a/web-app/src/containers/ModelSetting.tsx b/web-app/src/containers/ModelSetting.tsx index 9a3bfd814..079b735aa 100644 --- a/web-app/src/containers/ModelSetting.tsx +++ b/web-app/src/containers/ModelSetting.tsx @@ -14,7 +14,7 @@ import { Button } from '@/components/ui/button' import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting' import { useModelProvider } from '@/hooks/useModelProvider' import { useServiceHub } from '@/hooks/useServiceHub' -import { cn } from '@/lib/utils' +import { cn, getModelDisplayName } from '@/lib/utils' import { useTranslation } from '@/i18n/react-i18next-compat' type ModelSettingProps = { @@ -261,7 +261,7 @@ export function ModelSetting({ - {t('common:modelSettings.title', { modelId: model.id })} + {t('common:modelSettings.title', { modelId: getModelDisplayName(model) })} {t('common:modelSettings.description')} diff --git a/web-app/src/containers/__tests__/AvatarEmoji.test.tsx b/web-app/src/containers/__tests__/AvatarEmoji.test.tsx index ea44b95a0..e0ec9488a 100644 --- a/web-app/src/containers/__tests__/AvatarEmoji.test.tsx +++ b/web-app/src/containers/__tests__/AvatarEmoji.test.tsx @@ -121,4 +121,4 @@ describe('AvatarEmoji Component', () => { const img = screen.getByRole('img') expect(img).toHaveAttribute('alt', 'Custom avatar') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/ChatInput.simple.test.tsx b/web-app/src/containers/__tests__/ChatInput.simple.test.tsx index a1c71baa8..8992edbc0 100644 --- a/web-app/src/containers/__tests__/ChatInput.simple.test.tsx +++ b/web-app/src/containers/__tests__/ChatInput.simple.test.tsx @@ -36,4 +36,4 @@ describe('ChatInput Simple Tests', () => { const sendButton = screen.getByTestId('send-message-button') expect(sendButton).toHaveTextContent('Send') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/ChatInput.test.tsx b/web-app/src/containers/__tests__/ChatInput.test.tsx index 50b0b6172..9184e3b98 100644 --- a/web-app/src/containers/__tests__/ChatInput.test.tsx +++ b/web-app/src/containers/__tests__/ChatInput.test.tsx @@ -392,4 +392,4 @@ describe('ChatInput', () => { expect(() => renderWithRouter()).not.toThrow() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx b/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx new file mode 100644 index 000000000..5f5fba96a --- /dev/null +++ b/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx @@ -0,0 +1,277 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import DropdownModelProvider from '../DropdownModelProvider' +import { getModelDisplayName } from '@/lib/utils' +import { useModelProvider } from '@/hooks/useModelProvider' + +// Define basic types to avoid missing declarations +type ModelProvider = { + provider: string + active: boolean + models: Array<{ + id: string + displayName?: string + capabilities: string[] + }> + settings: unknown[] +} + +type Model = { + id: string + displayName?: string + capabilities?: string[] +} + +type MockHookReturn = { + providers: ModelProvider[] + selectedProvider: string + selectedModel: Model + getProviderByName: (name: string) => ModelProvider | undefined + selectModelProvider: () => void + getModelBy: (id: string) => Model | undefined + updateProvider: () => void +} + +// Mock the dependencies +vi.mock('@/hooks/useModelProvider', () => ({ + useModelProvider: vi.fn(), +})) + +vi.mock('@/hooks/useThreads', () => ({ + useThreads: vi.fn(() => ({ + updateCurrentThreadModel: vi.fn(), + })), +})) + +vi.mock('@/hooks/useServiceHub', () => ({ + useServiceHub: vi.fn(() => ({ + models: () => ({ + checkMmprojExists: vi.fn(() => Promise.resolve(false)), + checkMmprojExistsAndUpdateOffloadMMprojSetting: vi.fn(() => Promise.resolve()), + }), + })), +})) + +vi.mock('@/i18n/react-i18next-compat', () => ({ + useTranslation: vi.fn(() => ({ + t: (key: string) => key, + })), +})) + +vi.mock('@tanstack/react-router', () => ({ + useNavigate: vi.fn(() => vi.fn()), +})) + +vi.mock('@/hooks/useFavoriteModel', () => ({ + useFavoriteModel: vi.fn(() => ({ + favoriteModels: [], + })), +})) + +vi.mock('@/lib/platform/const', () => ({ + PlatformFeatures: { + WEB_AUTO_MODEL_SELECTION: false, + MODEL_PROVIDER_SETTINGS: true, + }, +})) + +// Mock UI components +vi.mock('@/components/ui/popover', () => ({ + Popover: ({ children }: { children: React.ReactNode }) =>
    {children}
    , + PopoverTrigger: ({ children }: { children: React.ReactNode }) => ( +
    {children}
    + ), + PopoverContent: ({ children }: { children: React.ReactNode }) => ( +
    {children}
    + ), +})) + +vi.mock('../ProvidersAvatar', () => ({ + default: ({ provider }: { provider: any }) => ( +
    + ), +})) + +vi.mock('../Capabilities', () => ({ + default: ({ capabilities }: { capabilities: string[] }) => ( +
    {capabilities.join(',')}
    + ), +})) + +vi.mock('../ModelSetting', () => ({ + ModelSetting: () =>
    , +})) + +vi.mock('../ModelSupportStatus', () => ({ + ModelSupportStatus: () =>
    , +})) + +describe('DropdownModelProvider - Display Name Integration', () => { + const mockProviders: ModelProvider[] = [ + { + provider: 'llamacpp', + active: true, + models: [ + { + id: 'model1.gguf', + displayName: 'Custom Model 1', + capabilities: ['completion'], + }, + { + id: 'model2-very-long-filename.gguf', + displayName: 'Short Name', + capabilities: ['completion'], + }, + { + id: 'model3.gguf', + // No displayName - should fall back to ID + capabilities: ['completion'], + }, + ], + settings: [], + }, + ] + + const mockSelectedModel = { + id: 'model1.gguf', + displayName: 'Custom Model 1', + capabilities: ['completion'], + } + + beforeEach(() => { + vi.clearAllMocks() + + // Reset the mock for each test + vi.mocked(useModelProvider).mockReturnValue({ + providers: mockProviders, + selectedProvider: 'llamacpp', + selectedModel: mockSelectedModel, + getProviderByName: vi.fn((name: string) => + mockProviders.find((p: ModelProvider) => p.provider === name) + ), + selectModelProvider: vi.fn(), + getModelBy: vi.fn((id: string) => + mockProviders[0].models.find((m: Model) => m.id === id) + ), + updateProvider: vi.fn(), + } as MockHookReturn) + }) + + it('should display custom model name in the trigger button', () => { + render() + + // Should show the display name in both trigger and dropdown + expect(screen.getAllByText('Custom Model 1')).toHaveLength(2) // One in trigger, one in dropdown + // Model ID should not be visible as text (it's only in title attributes) + expect(screen.queryByDisplayValue('model1.gguf')).not.toBeInTheDocument() + }) + + it('should fall back to model ID when no displayName is set', () => { + vi.mocked(useModelProvider).mockReturnValue({ + providers: mockProviders, + selectedProvider: 'llamacpp', + selectedModel: mockProviders[0].models[2], // model3 without displayName + getProviderByName: vi.fn((name: string) => + mockProviders.find((p: ModelProvider) => p.provider === name) + ), + selectModelProvider: vi.fn(), + getModelBy: vi.fn((id: string) => + mockProviders[0].models.find((m: Model) => m.id === id) + ), + updateProvider: vi.fn(), + } as MockHookReturn) + + render() + + expect(screen.getAllByText('model3.gguf')).toHaveLength(2) // Trigger and dropdown + }) + + it('should show display names in the model list items', () => { + render() + + // Check if the display names are shown in the options + expect(screen.getAllByText('Custom Model 1')).toHaveLength(2) // Selected: Trigger + dropdown + expect(screen.getByText('Short Name')).toBeInTheDocument() // Only in dropdown + expect(screen.getByText('model3.gguf')).toBeInTheDocument() // Only in dropdown + }) + + it('should use getModelDisplayName utility correctly', () => { + // Test the utility function directly with different model scenarios + const modelWithDisplayName = { + id: 'long-model-name.gguf', + displayName: 'Short Name', + } as Model + + const modelWithoutDisplayName = { + id: 'model-without-display-name.gguf', + } as Model + + const modelWithEmptyDisplayName = { + id: 'model-with-empty.gguf', + displayName: '', + } as Model + + expect(getModelDisplayName(modelWithDisplayName)).toBe('Short Name') + expect(getModelDisplayName(modelWithoutDisplayName)).toBe('model-without-display-name.gguf') + expect(getModelDisplayName(modelWithEmptyDisplayName)).toBe('model-with-empty.gguf') + }) + + it('should maintain model ID for internal operations while showing display name', () => { + const mockSelectModelProvider = vi.fn() + + vi.mocked(useModelProvider).mockReturnValue({ + providers: mockProviders, + selectedProvider: 'llamacpp', + selectedModel: mockSelectedModel, + getProviderByName: vi.fn((name: string) => + mockProviders.find((p: ModelProvider) => p.provider === name) + ), + selectModelProvider: mockSelectModelProvider, + getModelBy: vi.fn((id: string) => + mockProviders[0].models.find((m: Model) => m.id === id) + ), + updateProvider: vi.fn(), + } as MockHookReturn) + + render() + + // Verify that display name is shown in UI + expect(screen.getAllByText('Custom Model 1')).toHaveLength(2) // Trigger + dropdown + + // The actual model ID should still be preserved for backend operations + // This would be tested in the click handlers, but that requires more complex mocking + expect(mockSelectedModel.id).toBe('model1.gguf') + }) + + it('should handle updating display model when selection changes', () => { + // Test that when a new model is selected, the trigger updates correctly + // First render with model1 selected + const { rerender } = render() + + // Check trigger shows Custom Model 1 + const triggerButton = screen.getByRole('button') + expect(triggerButton).toHaveTextContent('Custom Model 1') + + // Update to select model2 + vi.mocked(useModelProvider).mockReturnValue({ + providers: mockProviders, + selectedProvider: 'llamacpp', + selectedModel: mockProviders[0].models[1], // model2 + getProviderByName: vi.fn((name: string) => + mockProviders.find((p: ModelProvider) => p.provider === name) + ), + selectModelProvider: vi.fn(), + getModelBy: vi.fn((id: string) => + mockProviders[0].models.find((m: Model) => m.id === id) + ), + updateProvider: vi.fn(), + } as MockHookReturn) + + rerender() + // Check trigger now shows Short Name + expect(triggerButton).toHaveTextContent('Short Name') + // Both models are still visible in the dropdown, so we can't test for absence + expect(screen.getAllByText('Short Name')).toHaveLength(2) // trigger + dropdown + }) +}) diff --git a/web-app/src/containers/__tests__/EditModel.test.tsx b/web-app/src/containers/__tests__/EditModel.test.tsx new file mode 100644 index 000000000..9f4eafc84 --- /dev/null +++ b/web-app/src/containers/__tests__/EditModel.test.tsx @@ -0,0 +1,184 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { DialogEditModel } from '../dialogs/EditModel' +import { useModelProvider } from '@/hooks/useModelProvider' +import '@testing-library/jest-dom' + +// Mock the dependencies +vi.mock('@/hooks/useModelProvider', () => ({ + useModelProvider: vi.fn(() => ({ + updateProvider: vi.fn(), + setProviders: vi.fn(), + })), +})) + +vi.mock('@/hooks/useServiceHub', () => ({ + useServiceHub: vi.fn(() => ({ + providers: () => ({ + getProviders: vi.fn(() => Promise.resolve([])), + }), + })), +})) + +vi.mock('@/i18n/react-i18next-compat', () => ({ + useTranslation: vi.fn(() => ({ + t: (key: string) => key, + })), +})) + +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})) + +// Mock Dialog components +vi.mock('@/components/ui/dialog', () => ({ + Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => + open ?
    {children}
    : null, + DialogContent: ({ children }: { children: React.ReactNode }) => ( +
    {children}
    + ), + DialogHeader: ({ children }: { children: React.ReactNode }) => ( +
    {children}
    + ), + DialogTitle: ({ children }: { children: React.ReactNode }) => ( +

    {children}

    + ), + DialogDescription: ({ children }: { children: React.ReactNode }) => ( +

    {children}

    + ), + DialogTrigger: ({ children }: { children: React.ReactNode }) => ( +
    {children}
    + ), +})) + +vi.mock('@/components/ui/input', () => ({ + Input: ({ value, onChange, ...props }: any) => ( + + ), +})) + +vi.mock('@/components/ui/button', () => ({ + Button: ({ children, onClick, ...props }: any) => ( + + ), +})) + +// Mock other UI components +vi.mock('@tabler/icons-react', () => ({ + IconPencil: () =>
    , + IconCheck: () =>
    , + IconX: () =>
    , + IconAlertTriangle: () =>
    , + IconEye: () =>
    , + IconTool: () =>
    , + IconLoader2: () =>
    , +})) + +describe('DialogEditModel - Basic Component Tests', () => { + const mockProvider = { + provider: 'llamacpp', + active: true, + models: [ + { + id: 'test-model.gguf', + displayName: 'My Custom Model', + capabilities: ['completion'], + }, + ], + settings: [], + } as any + + const mockUpdateProvider = vi.fn() + const mockSetProviders = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + + vi.mocked(useModelProvider).mockReturnValue({ + updateProvider: mockUpdateProvider, + setProviders: mockSetProviders, + } as any) + }) + + it('should render without errors', () => { + const { container } = render( + + ) + + // Component should render without throwing errors + expect(container).toBeInTheDocument() + }) + + it('should handle provider without models', () => { + const emptyProvider = { + ...mockProvider, + models: [], + } as any + + const { container } = render( + + ) + + // Component should handle empty models gracefully + expect(container).toBeInTheDocument() + }) + + it('should accept provider and modelId props', () => { + const { container } = render( + + ) + + expect(container).toBeInTheDocument() + }) + + it('should not crash with minimal props', () => { + const minimalProvider = { + provider: 'test', + active: false, + models: [], + settings: [], + } as any + + expect(() => { + render( + + ) + }).not.toThrow() + }) + + it('should have mocked dependencies available', () => { + render( + + ) + + // Verify our mocks are in place + expect(mockUpdateProvider).toBeDefined() + expect(mockSetProviders).toBeDefined() + }) +}) diff --git a/web-app/src/containers/__tests__/LeftPanel.test.tsx b/web-app/src/containers/__tests__/LeftPanel.test.tsx index e5b316e34..d8fcccc33 100644 --- a/web-app/src/containers/__tests__/LeftPanel.test.tsx +++ b/web-app/src/containers/__tests__/LeftPanel.test.tsx @@ -266,4 +266,4 @@ describe('LeftPanel', () => { const toggleButton = document.querySelector('svg.tabler-icon-layout-sidebar') expect(toggleButton).not.toBeNull() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/SetupScreen.test.tsx b/web-app/src/containers/__tests__/SetupScreen.test.tsx index 2fd26429b..1ef654eee 100644 --- a/web-app/src/containers/__tests__/SetupScreen.test.tsx +++ b/web-app/src/containers/__tests__/SetupScreen.test.tsx @@ -143,4 +143,4 @@ describe('SetupScreen', () => { const setupContent = screen.getByText('setup:welcome').closest('div') expect(setupContent).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx b/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx index 34d8b4380..2af8117af 100644 --- a/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx +++ b/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx @@ -75,4 +75,4 @@ export function DeleteAssistantDialog({ ) -} \ No newline at end of file +} diff --git a/web-app/src/containers/dialogs/EditModel.tsx b/web-app/src/containers/dialogs/EditModel.tsx index e1406f4f0..67576fbd6 100644 --- a/web-app/src/containers/dialogs/EditModel.tsx +++ b/web-app/src/containers/dialogs/EditModel.tsx @@ -23,7 +23,6 @@ import { } from '@tabler/icons-react' import { useState, useEffect } from 'react' import { useTranslation } from '@/i18n/react-i18next-compat' -import { useServiceHub } from '@/hooks/useServiceHub' import { toast } from 'sonner' // No need to define our own interface, we'll use the existing Model type @@ -37,16 +36,15 @@ export const DialogEditModel = ({ modelId, }: DialogEditModelProps) => { const { t } = useTranslation() - const { updateProvider, setProviders } = useModelProvider() + const { updateProvider } = useModelProvider() const [selectedModelId, setSelectedModelId] = useState('') - const [modelName, setModelName] = useState('') - const [originalModelName, setOriginalModelName] = useState('') + const [displayName, setDisplayName] = useState('') + const [originalDisplayName, setOriginalDisplayName] = useState('') const [originalCapabilities, setOriginalCapabilities] = useState< Record >({}) const [isOpen, setIsOpen] = useState(false) const [isLoading, setIsLoading] = useState(false) - const serviceHub = useServiceHub() const [capabilities, setCapabilities] = useState>({ completion: false, vision: false, @@ -82,11 +80,10 @@ export const DialogEditModel = ({ // Get the currently selected model const selectedModel = provider.models.find( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (m: any) => m.id === selectedModelId + (m: Model) => m.id === selectedModelId ) - // Initialize capabilities and model name from selected model + // Initialize capabilities and display name from selected model useEffect(() => { if (selectedModel) { const modelCapabilities = selectedModel.capabilities || [] @@ -98,9 +95,10 @@ export const DialogEditModel = ({ web_search: modelCapabilities.includes('web_search'), reasoning: modelCapabilities.includes('reasoning'), }) - const modelNameValue = selectedModel.id - setModelName(modelNameValue) - setOriginalModelName(modelNameValue) + // Use existing displayName if available, otherwise fall back to model ID + const displayNameValue = (selectedModel as Model & { displayName?: string }).displayName || selectedModel.id + setDisplayName(displayNameValue) + setOriginalDisplayName(displayNameValue) const originalCaps = { completion: modelCapabilities.includes('completion'), @@ -122,14 +120,14 @@ export const DialogEditModel = ({ })) } - // Handle model name change - const handleModelNameChange = (newName: string) => { - setModelName(newName) + // Handle display name change + const handleDisplayNameChange = (newName: string) => { + setDisplayName(newName) } // Check if there are unsaved changes const hasUnsavedChanges = () => { - const nameChanged = modelName !== originalModelName + const nameChanged = displayName !== originalDisplayName const capabilitiesChanged = JSON.stringify(capabilities) !== JSON.stringify(originalCapabilities) return nameChanged || capabilitiesChanged @@ -141,13 +139,21 @@ export const DialogEditModel = ({ setIsLoading(true) try { - // Update model name if changed - if (modelName !== originalModelName) { - await serviceHub - .models() - .updateModel(selectedModel.id, { id: modelName }) - setOriginalModelName(modelName) - await serviceHub.providers().getProviders().then(setProviders) + let updatedModels = provider.models + + // Update display name if changed + if (displayName !== originalDisplayName) { + // Update the model in the provider models array with displayName + updatedModels = updatedModels.map((m: Model) => { + if (m.id === selectedModelId) { + return { + ...m, + displayName: displayName, + } + } + return m + }) + setOriginalDisplayName(displayName) } // Update capabilities if changed @@ -159,8 +165,7 @@ export const DialogEditModel = ({ .map(([capName]) => capName) // Find and update the model in the provider - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const updatedModels = provider.models.map((m: any) => { + updatedModels = updatedModels.map((m: Model) => { if (m.id === selectedModelId) { return { ...m, @@ -172,15 +177,15 @@ export const DialogEditModel = ({ return m }) - // Update the provider with the updated models - updateProvider(provider.provider, { - ...provider, - models: updatedModels, - }) - setOriginalCapabilities(capabilities) } + // Update the provider with the updated models + updateProvider(provider.provider, { + ...provider, + models: updatedModels, + }) + // Show success toast and close dialog toast.success('Model updated successfully') setIsOpen(false) @@ -213,22 +218,25 @@ export const DialogEditModel = ({ - {/* Model Name Section */} + {/* Model Display Name Section */}
    handleModelNameChange(e.target.value)} - placeholder="Enter model name" + id="display-name" + value={displayName} + onChange={(e) => handleDisplayNameChange(e.target.value)} + placeholder="Enter display name" className="w-full" disabled={isLoading} /> +

    + This is the name that will be shown in the interface. The original model file remains unchanged. +

    {/* Warning Banner */} diff --git a/web-app/src/containers/dialogs/FactoryResetDialog.tsx b/web-app/src/containers/dialogs/FactoryResetDialog.tsx index 927676799..644460b2f 100644 --- a/web-app/src/containers/dialogs/FactoryResetDialog.tsx +++ b/web-app/src/containers/dialogs/FactoryResetDialog.tsx @@ -77,4 +77,4 @@ export function FactoryResetDialog({ ) -} \ No newline at end of file +} diff --git a/web-app/src/hooks/__tests__/useAnalytic.test.ts b/web-app/src/hooks/__tests__/useAnalytic.test.ts index 8ecf4c18c..fa26ae7b9 100644 --- a/web-app/src/hooks/__tests__/useAnalytic.test.ts +++ b/web-app/src/hooks/__tests__/useAnalytic.test.ts @@ -142,4 +142,4 @@ describe('useAnalytic', () => { expect(result.current.productAnalytic).toBe(false) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useAppState.test.ts b/web-app/src/hooks/__tests__/useAppState.test.ts index a677b6564..462096eac 100644 --- a/web-app/src/hooks/__tests__/useAppState.test.ts +++ b/web-app/src/hooks/__tests__/useAppState.test.ts @@ -201,4 +201,4 @@ describe('useAppState', () => { expect(result.current.tokenSpeed).toBeUndefined() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useAppUpdater.test.ts b/web-app/src/hooks/__tests__/useAppUpdater.test.ts index 1cbd96afe..fd1a41482 100644 --- a/web-app/src/hooks/__tests__/useAppUpdater.test.ts +++ b/web-app/src/hooks/__tests__/useAppUpdater.test.ts @@ -406,4 +406,4 @@ describe('useAppUpdater', () => { expect(mockEvents.emit).toHaveBeenCalledWith('onAppUpdateDownloadSuccess', {}) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useAppearance.test.ts b/web-app/src/hooks/__tests__/useAppearance.test.ts index 74be4d3d2..8a1f3437c 100644 --- a/web-app/src/hooks/__tests__/useAppearance.test.ts +++ b/web-app/src/hooks/__tests__/useAppearance.test.ts @@ -285,4 +285,4 @@ describe('useAppearance', () => { expect(result.current.chatWidth).toBe('compact') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useChat.test.ts b/web-app/src/hooks/__tests__/useChat.test.ts index 45d46eb53..e87191fb6 100644 --- a/web-app/src/hooks/__tests__/useChat.test.ts +++ b/web-app/src/hooks/__tests__/useChat.test.ts @@ -225,4 +225,4 @@ describe('useChat', () => { expect(result.current).toBeDefined() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useClickOutside.test.ts b/web-app/src/hooks/__tests__/useClickOutside.test.ts index 0752c9396..405040761 100644 --- a/web-app/src/hooks/__tests__/useClickOutside.test.ts +++ b/web-app/src/hooks/__tests__/useClickOutside.test.ts @@ -171,4 +171,4 @@ describe('useClickOutside', () => { addEventListenerSpy.mockRestore() removeEventListenerSpy.mockRestore() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useCodeblock.test.ts b/web-app/src/hooks/__tests__/useCodeblock.test.ts index 9a71fd381..187af34b2 100644 --- a/web-app/src/hooks/__tests__/useCodeblock.test.ts +++ b/web-app/src/hooks/__tests__/useCodeblock.test.ts @@ -147,4 +147,4 @@ describe('useCodeblock', () => { expect(result.current.codeBlockStyle).toBe('preserved-theme') expect(result.current.showLineNumbers).toBe(false) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useDownloadStore.test.ts b/web-app/src/hooks/__tests__/useDownloadStore.test.ts index 66c5c97de..e68116489 100644 --- a/web-app/src/hooks/__tests__/useDownloadStore.test.ts +++ b/web-app/src/hooks/__tests__/useDownloadStore.test.ts @@ -259,4 +259,4 @@ describe('useDownloadStore', () => { expect(result.current.localDownloadingModels.has('model-1')).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useHotkeys.test.ts b/web-app/src/hooks/__tests__/useHotkeys.test.ts index 9af15ab8a..621718eb5 100644 --- a/web-app/src/hooks/__tests__/useHotkeys.test.ts +++ b/web-app/src/hooks/__tests__/useHotkeys.test.ts @@ -468,4 +468,4 @@ describe('useKeyboardShortcut', () => { expect(mockCallback).toHaveBeenCalledTimes(1) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useLeftPanel.test.ts b/web-app/src/hooks/__tests__/useLeftPanel.test.ts index 964ef3dd8..9406ac599 100644 --- a/web-app/src/hooks/__tests__/useLeftPanel.test.ts +++ b/web-app/src/hooks/__tests__/useLeftPanel.test.ts @@ -143,4 +143,4 @@ describe('useLeftPanel', () => { expect(result.current.open).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts b/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts index b8a5acdcf..6be0622e3 100644 --- a/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts +++ b/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts @@ -154,4 +154,4 @@ describe('useLlamacppDevices', () => { expect(result.current.devices[1].activated).toBe(true) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useLocalApiServer.test.ts b/web-app/src/hooks/__tests__/useLocalApiServer.test.ts index 388f438b9..877355651 100644 --- a/web-app/src/hooks/__tests__/useLocalApiServer.test.ts +++ b/web-app/src/hooks/__tests__/useLocalApiServer.test.ts @@ -663,4 +663,4 @@ describe('useLocalApiServer', () => { expect(result.current.serverPort).toBe(65535) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useMCPServers.test.ts b/web-app/src/hooks/__tests__/useMCPServers.test.ts index e5256a549..2e7810be1 100644 --- a/web-app/src/hooks/__tests__/useMCPServers.test.ts +++ b/web-app/src/hooks/__tests__/useMCPServers.test.ts @@ -474,4 +474,4 @@ describe('useMCPServers', () => { expect(result.current.deletedServerKeys).toContain('lifecycle-server') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useMediaQuery.test.ts b/web-app/src/hooks/__tests__/useMediaQuery.test.ts index 653511657..315fd9593 100644 --- a/web-app/src/hooks/__tests__/useMediaQuery.test.ts +++ b/web-app/src/hooks/__tests__/useMediaQuery.test.ts @@ -316,4 +316,4 @@ describe('useSmallScreen', () => { expect(mockMatchMedia).toHaveBeenCalledWith('(max-width: 768px)') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useMessages.test.ts b/web-app/src/hooks/__tests__/useMessages.test.ts index 89c0c4e85..503806e38 100644 --- a/web-app/src/hooks/__tests__/useMessages.test.ts +++ b/web-app/src/hooks/__tests__/useMessages.test.ts @@ -392,4 +392,4 @@ describe('useMessages', () => { expect(result2.current.getMessages('thread1')).toEqual([testMessage]) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useModelContextApproval.test.ts b/web-app/src/hooks/__tests__/useModelContextApproval.test.ts index 1c5e83dcd..8ed52b56b 100644 --- a/web-app/src/hooks/__tests__/useModelContextApproval.test.ts +++ b/web-app/src/hooks/__tests__/useModelContextApproval.test.ts @@ -311,4 +311,4 @@ describe('useContextSizeApproval', () => { expect(secondResult).toBe('context_shift') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useModelProvider.test.ts b/web-app/src/hooks/__tests__/useModelProvider.test.ts new file mode 100644 index 000000000..e1fc4a8d2 --- /dev/null +++ b/web-app/src/hooks/__tests__/useModelProvider.test.ts @@ -0,0 +1,182 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { act, renderHook } from '@testing-library/react' +import { useModelProvider } from '../useModelProvider' + +// Mock getServiceHub +vi.mock('@/hooks/useServiceHub', () => ({ + getServiceHub: vi.fn(() => ({ + path: () => ({ + sep: () => '/', + }), + })), +})) + +// Mock the localStorage key constants +vi.mock('@/constants/localStorage', () => ({ + localStorageKey: { + modelProvider: 'jan-model-provider', + }, +})) + +// Mock localStorage +const localStorageMock = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), +} +Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true, +}) + +describe('useModelProvider - displayName functionality', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorageMock.getItem.mockReturnValue(null) + + // Reset Zustand store to default state + act(() => { + useModelProvider.setState({ + providers: [], + selectedProvider: 'llamacpp', + selectedModel: null, + deletedModels: [], + }) + }) + }) + + it('should handle models without displayName property', () => { + const { result } = renderHook(() => useModelProvider()) + + const provider = { + provider: 'llamacpp', + active: true, + models: [ + { + id: 'test-model.gguf', + capabilities: ['completion'], + }, + ], + settings: [], + } as any + + // First add the provider, then update it (since updateProvider only updates existing providers) + act(() => { + result.current.addProvider(provider) + }) + + const updatedProvider = result.current.getProviderByName('llamacpp') + expect(updatedProvider?.models[0].displayName).toBeUndefined() + expect(updatedProvider?.models[0].id).toBe('test-model.gguf') + }) + + it('should preserve displayName when merging providers in setProviders', () => { + const { result } = renderHook(() => useModelProvider()) + + // First, set up initial state with displayName via direct state manipulation + // This simulates the scenario where a user has already customized a display name + act(() => { + useModelProvider.setState({ + providers: [ + { + provider: 'llamacpp', + active: true, + models: [ + { + id: 'test-model.gguf', + displayName: 'My Custom Model', + capabilities: ['completion'], + }, + ], + settings: [], + }, + ] as any, + selectedProvider: 'llamacpp', + selectedModel: null, + deletedModels: [], + }) + }) + + // Now simulate setProviders with fresh data (like from server refresh) + const freshProviders = [ + { + provider: 'llamacpp', + active: true, + persist: true, + models: [ + { + id: 'test-model.gguf', + capabilities: ['completion'], + // Note: no displayName in fresh data + }, + ], + settings: [], + }, + ] as any + + act(() => { + result.current.setProviders(freshProviders) + }) + + // The displayName should be preserved from existing state + const provider = result.current.getProviderByName('llamacpp') + expect(provider?.models[0].displayName).toBe('My Custom Model') + }) + + it('should provide basic functionality without breaking existing behavior', () => { + const { result } = renderHook(() => useModelProvider()) + + // Test that basic provider operations work + expect(result.current.providers).toEqual([]) + expect(result.current.selectedProvider).toBe('llamacpp') + expect(result.current.selectedModel).toBeNull() + + // Test addProvider functionality + const provider = { + provider: 'openai', + active: true, + models: [], + settings: [], + } as any + + act(() => { + result.current.addProvider(provider) + }) + + expect(result.current.providers).toHaveLength(1) + expect(result.current.getProviderByName('openai')).toBeDefined() + }) + + it('should handle provider operations with models that have displayName', () => { + const { result } = renderHook(() => useModelProvider()) + + // Test that we can at least get and set providers with displayName models + const providerWithDisplayName = { + provider: 'llamacpp', + active: true, + models: [ + { + id: 'test-model.gguf', + displayName: 'Custom Model Name', + capabilities: ['completion'], + }, + ], + settings: [], + } as any + + // Set the state directly (simulating what would happen in real usage) + act(() => { + useModelProvider.setState({ + providers: [providerWithDisplayName], + selectedProvider: 'llamacpp', + selectedModel: null, + deletedModels: [], + }) + }) + + const provider = result.current.getProviderByName('llamacpp') + expect(provider?.models[0].displayName).toBe('Custom Model Name') + expect(provider?.models[0].id).toBe('test-model.gguf') + }) +}) diff --git a/web-app/src/hooks/__tests__/usePrompt.test.ts b/web-app/src/hooks/__tests__/usePrompt.test.ts index b7997cf1b..4a56cd38b 100644 --- a/web-app/src/hooks/__tests__/usePrompt.test.ts +++ b/web-app/src/hooks/__tests__/usePrompt.test.ts @@ -98,4 +98,4 @@ describe('usePrompt', () => { expect(result.current.prompt).toBe(longText) expect(result.current.prompt.length).toBe(10000) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useProviderModels.test.ts b/web-app/src/hooks/__tests__/useProviderModels.test.ts index da9b60e07..e19bcb102 100644 --- a/web-app/src/hooks/__tests__/useProviderModels.test.ts +++ b/web-app/src/hooks/__tests__/useProviderModels.test.ts @@ -99,4 +99,4 @@ describe('useProviderModels', () => { expect(fetchModelsSpy).not.toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useProxyConfig.test.ts b/web-app/src/hooks/__tests__/useProxyConfig.test.ts index e411ad226..9aca90821 100644 --- a/web-app/src/hooks/__tests__/useProxyConfig.test.ts +++ b/web-app/src/hooks/__tests__/useProxyConfig.test.ts @@ -320,4 +320,4 @@ describe('useProxyConfig', () => { expect(typeof result.current.setNoProxy).toBe('function') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useReleaseNotes.test.ts b/web-app/src/hooks/__tests__/useReleaseNotes.test.ts index 1b84924ff..824ae0dfa 100644 --- a/web-app/src/hooks/__tests__/useReleaseNotes.test.ts +++ b/web-app/src/hooks/__tests__/useReleaseNotes.test.ts @@ -359,4 +359,4 @@ describe('useReleaseNotes', () => { expect(result.current.error).toBe(null) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useTheme.test.ts b/web-app/src/hooks/__tests__/useTheme.test.ts index a2416eb03..f0c294385 100644 --- a/web-app/src/hooks/__tests__/useTheme.test.ts +++ b/web-app/src/hooks/__tests__/useTheme.test.ts @@ -186,4 +186,4 @@ describe('useTheme', () => { expect(result.current.isDark).toBe(false) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useToolApproval.test.ts b/web-app/src/hooks/__tests__/useToolApproval.test.ts index e73e193a3..2db153543 100644 --- a/web-app/src/hooks/__tests__/useToolApproval.test.ts +++ b/web-app/src/hooks/__tests__/useToolApproval.test.ts @@ -440,4 +440,4 @@ describe('useToolApproval', () => { expect(result.current.isToolApproved('thread-1', 'tool-a')).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useToolAvailable.test.ts b/web-app/src/hooks/__tests__/useToolAvailable.test.ts index f9387a4b3..b88762212 100644 --- a/web-app/src/hooks/__tests__/useToolAvailable.test.ts +++ b/web-app/src/hooks/__tests__/useToolAvailable.test.ts @@ -417,4 +417,4 @@ describe('useToolAvailable', () => { expect(result.current.isToolDisabled('thread-1', 'tool-c')).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useTools.test.ts b/web-app/src/hooks/__tests__/useTools.test.ts index f60b4bf18..82561e2db 100644 --- a/web-app/src/hooks/__tests__/useTools.test.ts +++ b/web-app/src/hooks/__tests__/useTools.test.ts @@ -179,4 +179,4 @@ describe('useTools', () => { expect(mockGetTools).toHaveBeenCalledTimes(1) expect(mockListen).toHaveBeenCalledTimes(1) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useVulkan.test.ts b/web-app/src/hooks/__tests__/useVulkan.test.ts index a958279be..aaf88057c 100644 --- a/web-app/src/hooks/__tests__/useVulkan.test.ts +++ b/web-app/src/hooks/__tests__/useVulkan.test.ts @@ -214,4 +214,4 @@ describe('useVulkan', () => { expect(result.current.vulkanEnabled).toBe(false) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 516a61b20..357fc3a8d 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -33,6 +33,7 @@ import { } from '@/utils/reasoning' import { useAssistant } from './useAssistant' import { useShallow } from 'zustand/shallow' +import { TEMPORARY_CHAT_QUERY_ID, TEMPORARY_CHAT_ID } from '@/constants/chat' export const useChat = () => { const [ @@ -80,12 +81,21 @@ export const useChat = () => { const getMessages = useMessages((state) => state.getMessages) const addMessage = useMessages((state) => state.addMessage) + const setMessages = useMessages((state) => state.setMessages) const setModelLoadError = useModelLoad((state) => state.setModelLoadError) const router = useRouter() const getCurrentThread = useCallback(async () => { let currentThread = retrieveThread() + // Check if we're in temporary chat mode + const isTemporaryMode = window.location.search.includes(`${TEMPORARY_CHAT_QUERY_ID}=true`) + + // Clear messages for existing temporary thread on reload to ensure fresh start + if (isTemporaryMode && currentThread?.id === TEMPORARY_CHAT_ID) { + setMessages(TEMPORARY_CHAT_ID, []) + } + if (!currentThread) { // Get prompt directly from store when needed const currentPrompt = usePrompt.getState().prompt @@ -93,14 +103,28 @@ export const useChat = () => { const assistants = useAssistant.getState().assistants const selectedModel = useModelProvider.getState().selectedModel const selectedProvider = useModelProvider.getState().selectedProvider + currentThread = await createThread( { id: selectedModel?.id ?? defaultModel(selectedProvider), provider: selectedProvider, }, - currentPrompt, - assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] + isTemporaryMode ? 'Temporary Chat' : currentPrompt, + assistants.find((a) => a.id === currentAssistant?.id) || assistants[0], + undefined, // no project metadata + isTemporaryMode // pass temporary flag ) + + // Clear messages for temporary chat to ensure fresh start on reload + if (isTemporaryMode && currentThread?.id === TEMPORARY_CHAT_ID) { + setMessages(TEMPORARY_CHAT_ID, []) + } + + // Set flag for temporary chat navigation + if (currentThread.id === TEMPORARY_CHAT_ID) { + sessionStorage.setItem('temp-chat-nav', 'true') + } + router.navigate({ to: route.threadsDetail, params: { threadId: currentThread.id }, diff --git a/web-app/src/hooks/useFavoriteModel.ts b/web-app/src/hooks/useFavoriteModel.ts index 979c1400e..778c9f9d0 100644 --- a/web-app/src/hooks/useFavoriteModel.ts +++ b/web-app/src/hooks/useFavoriteModel.ts @@ -50,4 +50,4 @@ export const useFavoriteModel = create()( storage: createJSONStorage(() => localStorage), } ) -) \ No newline at end of file +) diff --git a/web-app/src/hooks/useModelProvider.ts b/web-app/src/hooks/useModelProvider.ts index bd3dbc49b..a0b5a96ce 100644 --- a/web-app/src/hooks/useModelProvider.ts +++ b/web-app/src/hooks/useModelProvider.ts @@ -104,6 +104,7 @@ export const useModelProvider = create()( ...model, settings: settings, capabilities: existingModel?.capabilities || model.capabilities, + displayName: existingModel?.displayName || model.displayName, } }) diff --git a/web-app/src/hooks/useServiceHub.ts b/web-app/src/hooks/useServiceHub.ts index 22af1886b..0a0b00d05 100644 --- a/web-app/src/hooks/useServiceHub.ts +++ b/web-app/src/hooks/useServiceHub.ts @@ -52,4 +52,4 @@ export const initializeServiceHubStore = (serviceHub: ServiceHub) => { */ export const isServiceHubInitialized = (): boolean => { return useServiceStore.getState().serviceHub !== null -} \ No newline at end of file +} diff --git a/web-app/src/hooks/useThreads.ts b/web-app/src/hooks/useThreads.ts index b450874cd..d68e49853 100644 --- a/web-app/src/hooks/useThreads.ts +++ b/web-app/src/hooks/useThreads.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { ulid } from 'ulidx' import { getServiceHub } from '@/hooks/useServiceHub' import { Fzf } from 'fzf' +import { TEMPORARY_CHAT_ID } from '@/constants/chat' type ThreadState = { threads: Record @@ -21,7 +22,8 @@ type ThreadState = { model: ThreadModel, title?: string, assistant?: Assistant, - projectMetadata?: { id: string; name: string; updated_at: number } + projectMetadata?: { id: string; name: string; updated_at: number }, + isTemporary?: boolean ) => Promise updateCurrentThreadModel: (model: ThreadModel) => void getFilteredThreads: (searchTerm: string) => Thread[] @@ -61,9 +63,12 @@ export const useThreads = create()((set, get) => ({ }, {} as Record ) + // Filter out temporary chat for search index + const filteredForSearch = Object.values(threadMap).filter(t => t.id !== TEMPORARY_CHAT_ID) + set({ threads: threadMap, - searchIndex: new Fzf(Object.values(threadMap), { + searchIndex: new Fzf(filteredForSearch, { selector: (item: Thread) => item.title, }), }) @@ -71,15 +76,18 @@ export const useThreads = create()((set, get) => ({ getFilteredThreads: (searchTerm: string) => { const { threads, searchIndex } = get() + // Filter out temporary chat from all operations + const filteredThreadsValues = Object.values(threads).filter(t => t.id !== TEMPORARY_CHAT_ID) + // If no search term, return all threads if (!searchTerm) { // return all threads - return Object.values(threads) + return filteredThreadsValues } let currentIndex = searchIndex if (!currentIndex?.find) { - currentIndex = new Fzf(Object.values(threads), { + currentIndex = new Fzf(filteredThreadsValues, { selector: (item: Thread) => item.title, }) set({ searchIndex: currentIndex }) @@ -125,7 +133,7 @@ export const useThreads = create()((set, get) => ({ getServiceHub().threads().deleteThread(threadId) return { threads: remainingThreads, - searchIndex: new Fzf(Object.values(remainingThreads), { + searchIndex: new Fzf(Object.values(remainingThreads).filter(t => t.id !== TEMPORARY_CHAT_ID), { selector: (item: Thread) => item.title, }), } @@ -165,7 +173,7 @@ export const useThreads = create()((set, get) => ({ return { threads: remainingThreads, - searchIndex: new Fzf(Object.values(remainingThreads), { + searchIndex: new Fzf(Object.values(remainingThreads).filter(t => t.id !== TEMPORARY_CHAT_ID), { selector: (item: Thread) => item.title, }), } @@ -218,18 +226,24 @@ export const useThreads = create()((set, get) => ({ setCurrentThreadId: (threadId) => { if (threadId !== get().currentThreadId) set({ currentThreadId: threadId }) }, - createThread: async (model, title, assistant, projectMetadata) => { + createThread: async (model, title, assistant, projectMetadata, isTemporary) => { const newThread: Thread = { - id: ulid(), - title: title ?? 'New Thread', + id: isTemporary ? TEMPORARY_CHAT_ID : ulid(), + title: title ?? (isTemporary ? 'Temporary Chat' : 'New Thread'), model, updated: Date.now() / 1000, assistants: assistant ? [assistant] : [], - ...(projectMetadata && { + ...(projectMetadata && !isTemporary && { metadata: { project: projectMetadata, }, }), + ...(isTemporary && { + metadata: { + isTemporary: true, + ...(projectMetadata && { project: projectMetadata }), + }, + }), } return await getServiceHub() .threads() @@ -307,7 +321,7 @@ export const useThreads = create()((set, get) => ({ const newThreads = { ...state.threads, [threadId]: updatedThread } return { threads: newThreads, - searchIndex: new Fzf(Object.values(newThreads), { + searchIndex: new Fzf(Object.values(newThreads).filter(t => t.id !== TEMPORARY_CHAT_ID), { selector: (item: Thread) => item.title, }), } @@ -337,7 +351,7 @@ export const useThreads = create()((set, get) => ({ return { threads: updatedThreads, - searchIndex: new Fzf(Object.values(updatedThreads), { + searchIndex: new Fzf(Object.values(updatedThreads).filter(t => t.id !== TEMPORARY_CHAT_ID), { selector: (item: Thread) => item.title, }), } @@ -359,7 +373,7 @@ export const useThreads = create()((set, get) => ({ const newThreads = { ...state.threads, [threadId]: updatedThread } return { threads: newThreads, - searchIndex: new Fzf(Object.values(newThreads), { + searchIndex: new Fzf(Object.values(newThreads).filter(t => t.id !== TEMPORARY_CHAT_ID), { selector: (item: Thread) => item.title, }), } diff --git a/web-app/src/i18n.ts b/web-app/src/i18n.ts index 84abfe50c..7a8e34750 100644 --- a/web-app/src/i18n.ts +++ b/web-app/src/i18n.ts @@ -4,4 +4,4 @@ export { default } from '@/i18n/setup' // Re-export compatibility functions for existing code export { useTranslation } from '@/i18n/react-i18next-compat' export { useAppTranslation } from '@/i18n/hooks' -export { TranslationProvider } from '@/i18n/TranslationContext' \ No newline at end of file +export { TranslationProvider } from '@/i18n/TranslationContext' diff --git a/web-app/src/i18n/TranslationContext.tsx b/web-app/src/i18n/TranslationContext.tsx index de2643764..539afc03c 100644 --- a/web-app/src/i18n/TranslationContext.tsx +++ b/web-app/src/i18n/TranslationContext.tsx @@ -43,4 +43,4 @@ export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ childre ) } -export default TranslationProvider \ No newline at end of file +export default TranslationProvider diff --git a/web-app/src/i18n/context.ts b/web-app/src/i18n/context.ts index 49a3be692..35ba65880 100644 --- a/web-app/src/i18n/context.ts +++ b/web-app/src/i18n/context.ts @@ -8,4 +8,4 @@ export const TranslationContext = createContext<{ }>({ t: (key: string) => key, i18n: i18next, -}) \ No newline at end of file +}) diff --git a/web-app/src/i18n/hooks.ts b/web-app/src/i18n/hooks.ts index f1c09f7f5..f1fdf1d67 100644 --- a/web-app/src/i18n/hooks.ts +++ b/web-app/src/i18n/hooks.ts @@ -2,4 +2,4 @@ import { useContext } from "react" import { TranslationContext } from "./context" // Custom hook for easy translations -export const useAppTranslation = () => useContext(TranslationContext) \ No newline at end of file +export const useAppTranslation = () => useContext(TranslationContext) diff --git a/web-app/src/i18n/index.ts b/web-app/src/i18n/index.ts index 7c8f53bda..26f21b4d5 100644 --- a/web-app/src/i18n/index.ts +++ b/web-app/src/i18n/index.ts @@ -5,4 +5,4 @@ export { default as i18n, loadTranslations } from './setup' export { TranslationProvider } from './TranslationContext' // Export types -export type { I18nInstance, TranslationResources } from './setup' \ No newline at end of file +export type { I18nInstance, TranslationResources } from './setup' diff --git a/web-app/src/i18n/react-i18next-compat.ts b/web-app/src/i18n/react-i18next-compat.ts index 585287b9a..ee6799b9f 100644 --- a/web-app/src/i18n/react-i18next-compat.ts +++ b/web-app/src/i18n/react-i18next-compat.ts @@ -31,4 +31,4 @@ export { default as i18n } from './setup' // Re-export other utilities export { TranslationProvider } from './TranslationContext' -export { useAppTranslation } from './hooks' \ No newline at end of file +export { useAppTranslation } from './hooks' diff --git a/web-app/src/i18n/setup.ts b/web-app/src/i18n/setup.ts index 9026100ee..9b4f4621e 100644 --- a/web-app/src/i18n/setup.ts +++ b/web-app/src/i18n/setup.ts @@ -153,4 +153,4 @@ export const loadTranslations = (): void => { // Initialize and export the i18n instance const i18n = initI18n() -export default i18n \ No newline at end of file +export default i18n diff --git a/web-app/src/lib/__tests__/completion.test.ts b/web-app/src/lib/__tests__/completion.test.ts index 2ea67068d..2b3ccaec7 100644 --- a/web-app/src/lib/__tests__/completion.test.ts +++ b/web-app/src/lib/__tests__/completion.test.ts @@ -187,4 +187,4 @@ describe('completion.ts', () => { expect(result.length).toBe(0) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/lib/__tests__/extension.test.ts b/web-app/src/lib/__tests__/extension.test.ts index d4a2e8da9..b6419a30e 100644 --- a/web-app/src/lib/__tests__/extension.test.ts +++ b/web-app/src/lib/__tests__/extension.test.ts @@ -138,4 +138,4 @@ describe('extension.ts', () => { expect(invoke).toHaveBeenCalledWith('test_command', { param: 'value' }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/lib/__tests__/utils.test.ts b/web-app/src/lib/__tests__/utils.test.ts index 25bc91334..33c51447e 100644 --- a/web-app/src/lib/__tests__/utils.test.ts +++ b/web-app/src/lib/__tests__/utils.test.ts @@ -6,6 +6,7 @@ import { toGigabytes, formatMegaBytes, formatDuration, + getModelDisplayName, } from '../utils' describe('getProviderLogo', () => { @@ -200,3 +201,52 @@ describe('formatDuration', () => { expect(formatDuration(start, 86400000)).toBe('1d 0h 0m 0s') // exactly 1 day }) }) + +describe('getModelDisplayName', () => { + it('returns displayName when it exists', () => { + const model = { + id: 'llama-3.2-1b-instruct-q4_k_m.gguf', + displayName: 'My Custom Model', + } as Model + expect(getModelDisplayName(model)).toBe('My Custom Model') + }) + + it('returns model.id when displayName is undefined', () => { + const model = { + id: 'llama-3.2-1b-instruct-q4_k_m.gguf', + } as Model + expect(getModelDisplayName(model)).toBe('llama-3.2-1b-instruct-q4_k_m.gguf') + }) + + it('returns model.id when displayName is empty string', () => { + const model = { + id: 'llama-3.2-1b-instruct-q4_k_m.gguf', + displayName: '', + } as Model + expect(getModelDisplayName(model)).toBe('llama-3.2-1b-instruct-q4_k_m.gguf') + }) + + it('returns model.id when displayName is null', () => { + const model = { + id: 'llama-3.2-1b-instruct-q4_k_m.gguf', + displayName: null as any, + } as Model + expect(getModelDisplayName(model)).toBe('llama-3.2-1b-instruct-q4_k_m.gguf') + }) + + it('handles models with complex display names', () => { + const model = { + id: 'very-long-model-file-name-with-lots-of-details.gguf', + displayName: 'Short Name 🤖', + } as Model + expect(getModelDisplayName(model)).toBe('Short Name 🤖') + }) + + it('handles models with special characters in displayName', () => { + const model = { + id: 'model.gguf', + displayName: 'Model (Version 2.0) - Fine-tuned', + } as Model + expect(getModelDisplayName(model)).toBe('Model (Version 2.0) - Fine-tuned') + }) +}) diff --git a/web-app/src/lib/analytics.ts b/web-app/src/lib/analytics.ts index b7e35dc60..a0ff9090f 100644 --- a/web-app/src/lib/analytics.ts +++ b/web-app/src/lib/analytics.ts @@ -14,4 +14,4 @@ export function trackEvent( } window.gtag('event', eventName, parameters) -} \ No newline at end of file +} diff --git a/web-app/src/lib/platform/const.ts b/web-app/src/lib/platform/const.ts index ab5a14e79..40c39a810 100644 --- a/web-app/src/lib/platform/const.ts +++ b/web-app/src/lib/platform/const.ts @@ -64,4 +64,7 @@ export const PlatformFeatures: Record = { // First message persisted thread - enabled for web only [PlatformFeature.FIRST_MESSAGE_PERSISTED_THREAD]: !isPlatformTauri(), -} \ No newline at end of file + + // Temporary chat mode - enabled for web only + [PlatformFeature.TEMPORARY_CHAT]: !isPlatformTauri(), +} diff --git a/web-app/src/lib/platform/index.ts b/web-app/src/lib/platform/index.ts index 08d34d4cc..807e9261c 100644 --- a/web-app/src/lib/platform/index.ts +++ b/web-app/src/lib/platform/index.ts @@ -10,4 +10,4 @@ export * from './types' export * from './utils' // Re-export components -export * from './PlatformGuard' \ No newline at end of file +export * from './PlatformGuard' diff --git a/web-app/src/lib/platform/types.ts b/web-app/src/lib/platform/types.ts index d0152da32..d9cfc2a9b 100644 --- a/web-app/src/lib/platform/types.ts +++ b/web-app/src/lib/platform/types.ts @@ -66,4 +66,7 @@ export enum PlatformFeature { // First message persisted thread - web-only feature for storing first user message locally during thread creation FIRST_MESSAGE_PERSISTED_THREAD = 'firstMessagePersistedThread', + + // Temporary chat mode - web-only feature for ephemeral conversations like ChatGPT + TEMPORARY_CHAT = 'temporaryChat', } diff --git a/web-app/src/lib/shortcuts/const.ts b/web-app/src/lib/shortcuts/const.ts index 8cac19536..969ee3c33 100644 --- a/web-app/src/lib/shortcuts/const.ts +++ b/web-app/src/lib/shortcuts/const.ts @@ -38,4 +38,4 @@ export const PlatformShortcuts: ShortcutMap = { key: '-', usePlatformMetaKey: true, }, -} \ No newline at end of file +} diff --git a/web-app/src/lib/shortcuts/index.ts b/web-app/src/lib/shortcuts/index.ts index ceefa40a3..ff545fdae 100644 --- a/web-app/src/lib/shortcuts/index.ts +++ b/web-app/src/lib/shortcuts/index.ts @@ -6,4 +6,4 @@ */ export * from './types' -export * from './const' \ No newline at end of file +export * from './const' diff --git a/web-app/src/lib/shortcuts/types.ts b/web-app/src/lib/shortcuts/types.ts index 8ac96ae0e..1ce5104e0 100644 --- a/web-app/src/lib/shortcuts/types.ts +++ b/web-app/src/lib/shortcuts/types.ts @@ -20,4 +20,4 @@ export interface ShortcutSpec { metaKey?: boolean } -export type ShortcutMap = Record \ No newline at end of file +export type ShortcutMap = Record diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index d34937b04..b035600ab 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -3,6 +3,7 @@ import { twMerge } from 'tailwind-merge' import { ExtensionManager } from './extension' import path from "path" + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } @@ -23,6 +24,14 @@ export function basenameNoExt(filePath: string): string { return base.slice(0, -path.extname(base).length); } + +/** + * Get the display name for a model, falling back to the model ID if no display name is set + */ +export function getModelDisplayName(model: Model): string { + return model.displayName || model.id +} + export function getProviderLogo(provider: string) { switch (provider) { case 'jan': diff --git a/web-app/src/locales/en/chat.json b/web-app/src/locales/en/chat.json index 6f15759c4..5ca734fcc 100644 --- a/web-app/src/locales/en/chat.json +++ b/web-app/src/locales/en/chat.json @@ -1,6 +1,8 @@ { "welcome": "Hi, how are you?", "description": "How can I help you today?", + "temporaryChat": "Temporary Chat", + "temporaryChatDescription": "Start a temporary conversation that won't be saved to your chat history.", "status": { "empty": "No Chats Found" }, diff --git a/web-app/src/locales/en/common.json b/web-app/src/locales/en/common.json index c829dbdf8..2c8b8c09d 100644 --- a/web-app/src/locales/en/common.json +++ b/web-app/src/locales/en/common.json @@ -124,6 +124,10 @@ "error": "Error", "success": "Success", "warning": "Warning", + "conversationNotAvailable": "Conversation not available", + "conversationNotAvailableDescription": "The conversation you are trying to access is not available or has been deleted.", + "temporaryChat": "Temporary Chat", + "temporaryChatTooltip": "Temporary chat won't appear in your history", "noResultsFoundDesc": "We couldn't find any chats matching your search. Try a different keyword.", "searchModels": "Search models...", "searchStyles": "Search styles...", diff --git a/web-app/src/providers/ServiceHubProvider.tsx b/web-app/src/providers/ServiceHubProvider.tsx index 68aa43c19..0ab261ae4 100644 --- a/web-app/src/providers/ServiceHubProvider.tsx +++ b/web-app/src/providers/ServiceHubProvider.tsx @@ -23,4 +23,4 @@ export function ServiceHubProvider({ children }: ServiceHubProviderProps) { }, []) return <>{isReady && children} -} \ No newline at end of file +} diff --git a/web-app/src/providers/__tests__/DataProvider.test.tsx b/web-app/src/providers/__tests__/DataProvider.test.tsx index 9757c2b29..4e578f1b5 100644 --- a/web-app/src/providers/__tests__/DataProvider.test.tsx +++ b/web-app/src/providers/__tests__/DataProvider.test.tsx @@ -101,4 +101,4 @@ describe('DataProvider', () => { expect(screen.getByText('Test Child 1')).toBeInTheDocument() expect(screen.getByText('Test Child 2')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/providers/__tests__/ThemeProvider.test.tsx b/web-app/src/providers/__tests__/ThemeProvider.test.tsx index 232035874..e0cf5748c 100644 --- a/web-app/src/providers/__tests__/ThemeProvider.test.tsx +++ b/web-app/src/providers/__tests__/ThemeProvider.test.tsx @@ -79,4 +79,4 @@ describe('ThemeProvider', () => { // Should be called on mount expect(useTheme).toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts b/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts index 23b65b9ef..8dbaf4720 100644 --- a/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts +++ b/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts @@ -304,4 +304,4 @@ describe('HuggingFace Repository Conversion', () => { expect(result.quants[0].file_size).toBe('1024.0 GB') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/index.tsx b/web-app/src/routes/index.tsx index b4b208b9d..cd501db3d 100644 --- a/web-app/src/routes/index.tsx +++ b/web-app/src/routes/index.tsx @@ -13,18 +13,29 @@ type SearchParams = { id: string provider: string } + 'temporary-chat'?: boolean } import DropdownAssistant from '@/containers/DropdownAssistant' import { useEffect } from 'react' import { useThreads } from '@/hooks/useThreads' import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' +import { TEMPORARY_CHAT_QUERY_ID } from '@/constants/chat' export const Route = createFileRoute(route.home as any)({ component: Index, - validateSearch: (search: Record): SearchParams => ({ - model: search.model as SearchParams['model'], - }), + validateSearch: (search: Record): SearchParams => { + const result: SearchParams = { + model: search.model as SearchParams['model'], + } + + // Only include temporary-chat if it's explicitly true + if (search[TEMPORARY_CHAT_QUERY_ID] === 'true' || search[TEMPORARY_CHAT_QUERY_ID] === true) { + result['temporary-chat'] = true + } + + return result + }, }) function Index() { @@ -32,6 +43,7 @@ function Index() { const { providers } = useModelProvider() const search = useSearch({ from: route.home as any }) const selectedModel = search.model + const isTemporaryChat = search['temporary-chat'] const { setCurrentThreadId } = useThreads() // Conditional to check if there are any valid providers @@ -60,10 +72,10 @@ function Index() {

    - {t('chat:welcome')} + {isTemporaryChat ? t('chat:temporaryChat') : t('chat:welcome')}

    - {t('chat:description')} + {isTemporaryChat ? t('chat:temporaryChatDescription') : t('chat:description')}

    diff --git a/web-app/src/routes/settings/__tests__/appearance.test.tsx b/web-app/src/routes/settings/__tests__/appearance.test.tsx index 6b2727588..c7560ad70 100644 --- a/web-app/src/routes/settings/__tests__/appearance.test.tsx +++ b/web-app/src/routes/settings/__tests__/appearance.test.tsx @@ -235,4 +235,4 @@ describe('Appearance Settings Route', () => { const settingsMenu = screen.getByTestId('settings-menu') expect(settingsMenu).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/extensions.test.tsx b/web-app/src/routes/settings/__tests__/extensions.test.tsx index d7dcf22d6..a111ab741 100644 --- a/web-app/src/routes/settings/__tests__/extensions.test.tsx +++ b/web-app/src/routes/settings/__tests__/extensions.test.tsx @@ -226,4 +226,4 @@ describe('Extensions Settings Route', () => { const settingsContent = screen.getByTestId('settings-menu').nextElementSibling expect(settingsContent).toHaveClass('p-4', 'w-full', 'h-[calc(100%-32px)]', 'overflow-y-auto') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/hardware.test.tsx b/web-app/src/routes/settings/__tests__/hardware.test.tsx index 604ee639c..a57b5d7c7 100644 --- a/web-app/src/routes/settings/__tests__/hardware.test.tsx +++ b/web-app/src/routes/settings/__tests__/hardware.test.tsx @@ -182,4 +182,4 @@ describe('Hardware Settings', () => { expect(screen.queryByText('GPUs')).not.toBeInTheDocument() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/privacy.test.tsx b/web-app/src/routes/settings/__tests__/privacy.test.tsx index 57bc4b870..3c46a571c 100644 --- a/web-app/src/routes/settings/__tests__/privacy.test.tsx +++ b/web-app/src/routes/settings/__tests__/privacy.test.tsx @@ -184,4 +184,4 @@ describe('Privacy Settings Route', () => { fireEvent.click(analyticsSwitch) expect(analyticsSwitch).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/shortcuts.test.tsx b/web-app/src/routes/settings/__tests__/shortcuts.test.tsx index 4e9eb7641..d5ac24ab1 100644 --- a/web-app/src/routes/settings/__tests__/shortcuts.test.tsx +++ b/web-app/src/routes/settings/__tests__/shortcuts.test.tsx @@ -190,4 +190,4 @@ describe('Shortcuts Settings Route', () => { const contentArea = screen.getAllByTestId('card') expect(contentArea.length).toBeGreaterThan(0) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 0b29c7bd3..2ee868a1c 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -3,7 +3,7 @@ import { Card, CardItem } from '@/containers/Card' import HeaderPage from '@/containers/HeaderPage' import SettingsMenu from '@/containers/SettingsMenu' import { useModelProvider } from '@/hooks/useModelProvider' -import { cn, getProviderTitle } from '@/lib/utils' +import { cn, getProviderTitle, getModelDisplayName } from '@/lib/utils' import { createFileRoute, Link, @@ -767,7 +767,7 @@ function ProviderDetail() { className="font-medium line-clamp-1" title={model.id} > - {model.id} + {getModelDisplayName(model)}
    diff --git a/web-app/src/routes/settings/providers/__tests__/index.test.tsx b/web-app/src/routes/settings/providers/__tests__/index.test.tsx index e6a95e2bc..767775b64 100644 --- a/web-app/src/routes/settings/providers/__tests__/index.test.tsx +++ b/web-app/src/routes/settings/providers/__tests__/index.test.tsx @@ -303,4 +303,4 @@ describe('Providers Settings Route', () => { // With empty providers array, should still render the page structure expect(screen.getByTestId('card')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/threads/$threadId.tsx b/web-app/src/routes/threads/$threadId.tsx index 4b2c07592..adfa8b140 100644 --- a/web-app/src/routes/threads/$threadId.tsx +++ b/web-app/src/routes/threads/$threadId.tsx @@ -1,7 +1,9 @@ import { useEffect, useMemo, useRef } from 'react' -import { createFileRoute, useParams } from '@tanstack/react-router' +import { createFileRoute, useParams, redirect, useNavigate } from '@tanstack/react-router' import cloneDeep from 'lodash.clonedeep' import { cn } from '@/lib/utils' +import { toast } from 'sonner' +import { useTranslation } from '@/i18n/react-i18next-compat' import HeaderPage from '@/containers/HeaderPage' import { useThreads } from '@/hooks/useThreads' @@ -22,15 +24,63 @@ import { PlatformFeature } from '@/lib/platform/types' import ScrollToBottom from '@/containers/ScrollToBottom' import { PromptProgress } from '@/components/PromptProgress' import { ThreadPadding } from '@/containers/ThreadPadding' +import { TEMPORARY_CHAT_ID, TEMPORARY_CHAT_QUERY_ID } from '@/constants/chat' +import { useThreadScrolling } from '@/hooks/useThreadScrolling' +import { IconInfoCircle } from '@tabler/icons-react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' + +const CONVERSATION_NOT_FOUND_EVENT = 'conversation-not-found' + +const TemporaryChatIndicator = ({ t }: { t: (key: string) => string }) => { + return ( +
    + {t('common:temporaryChat')} + + +
    + +
    +
    + +

    {t('common:temporaryChatTooltip')}

    +
    +
    +
    + ) +} // as route.threadsDetail export const Route = createFileRoute('/threads/$threadId')({ + beforeLoad: ({ params }) => { + // Check if this is the temporary chat being accessed directly + if (params.threadId === TEMPORARY_CHAT_ID) { + // Check if we have the navigation flag in sessionStorage + const hasNavigationFlag = sessionStorage.getItem('temp-chat-nav') + + if (!hasNavigationFlag) { + // Direct access - redirect to home with query parameter + throw redirect({ + to: '/', + search: { [TEMPORARY_CHAT_QUERY_ID]: true }, + replace: true, + }) + } + + // Clear the flag immediately after checking + sessionStorage.removeItem('temp-chat-nav') + } + }, component: ThreadDetail, }) function ThreadDetail() { const serviceHub = useServiceHub() const { threadId } = useParams({ from: Route.id }) + const navigate = useNavigate() + const { t } = useTranslation() const setCurrentThreadId = useThreads((state) => state.setCurrentThreadId) const setCurrentAssistant = useAssistant((state) => state.setCurrentAssistant) const assistants = useAssistant((state) => state.assistants) @@ -49,6 +99,33 @@ function ThreadDetail() { const thread = useThreads(useShallow((state) => state.threads[threadId])) const scrollContainerRef = useRef(null) + + // Get padding height for ChatGPT-style message positioning + const { paddingHeight } = useThreadScrolling(threadId, scrollContainerRef) + + // Listen for conversation not found events + useEffect(() => { + const handleConversationNotFound = (event: CustomEvent) => { + const { threadId: notFoundThreadId } = event.detail + if (notFoundThreadId === threadId) { + // Skip error handling for temporary chat - it's expected to not exist on server + if (threadId === TEMPORARY_CHAT_ID) { + return + } + + toast.error(t('common:conversationNotAvailable'), { + description: t('common:conversationNotAvailableDescription') + }) + navigate({ to: '/', replace: true }) + } + } + + window.addEventListener(CONVERSATION_NOT_FOUND_EVENT, handleConversationNotFound as EventListener) + return () => { + window.removeEventListener(CONVERSATION_NOT_FOUND_EVENT, handleConversationNotFound as EventListener) + } + }, [threadId, navigate]) + useEffect(() => { setCurrentThreadId(threadId) const assistant = assistants.find( @@ -134,9 +211,15 @@ function ThreadDetail() {
    - {PlatformFeatures[PlatformFeature.ASSISTANTS] && ( - - )} +
    + {PlatformFeatures[PlatformFeature.ASSISTANTS] && ( + + )} +
    +
    + {threadId === TEMPORARY_CHAT_ID && } +
    +
    diff --git a/web-app/src/services/__tests__/analytic.test.ts b/web-app/src/services/__tests__/analytic.test.ts index 94e25610f..56f3b9c67 100644 --- a/web-app/src/services/__tests__/analytic.test.ts +++ b/web-app/src/services/__tests__/analytic.test.ts @@ -265,4 +265,4 @@ describe('DefaultAnalyticService', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/assistants.test.ts b/web-app/src/services/__tests__/assistants.test.ts index 8c7d96e2c..0fe92f6b7 100644 --- a/web-app/src/services/__tests__/assistants.test.ts +++ b/web-app/src/services/__tests__/assistants.test.ts @@ -130,4 +130,4 @@ describe('DefaultAssistantsService', () => { await expect(assistantsService.deleteAssistant(assistant)).rejects.toThrow('Failed to delete assistant') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/events.test.ts b/web-app/src/services/__tests__/events.test.ts index ab3d597f8..9781cee8f 100644 --- a/web-app/src/services/__tests__/events.test.ts +++ b/web-app/src/services/__tests__/events.test.ts @@ -142,4 +142,4 @@ describe('EventEmitter', () => { expect(handler).toHaveBeenCalledWith(complexData) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/hardware.test.ts b/web-app/src/services/__tests__/hardware.test.ts index f9a16155b..69ccb0423 100644 --- a/web-app/src/services/__tests__/hardware.test.ts +++ b/web-app/src/services/__tests__/hardware.test.ts @@ -262,4 +262,4 @@ describe('TauriHardwareService', () => { expect(vi.mocked(invoke)).toHaveBeenNthCalledWith(2, 'plugin:hardware|get_system_usage') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/mcp.test.ts b/web-app/src/services/__tests__/mcp.test.ts index 0f5e9d073..a2af0cc76 100644 --- a/web-app/src/services/__tests__/mcp.test.ts +++ b/web-app/src/services/__tests__/mcp.test.ts @@ -430,4 +430,4 @@ describe('TauriMCPService', () => { expect(result).toEqual(toolResult) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/messages.test.ts b/web-app/src/services/__tests__/messages.test.ts index 445a9e53a..21032bb76 100644 --- a/web-app/src/services/__tests__/messages.test.ts +++ b/web-app/src/services/__tests__/messages.test.ts @@ -158,4 +158,4 @@ describe('DefaultMessagesService', () => { await expect(messagesService.deleteMessage(threadId, messageId)).rejects.toThrow('Failed to delete message') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/models.test.ts b/web-app/src/services/__tests__/models.test.ts index 4322cfe40..d0c9daa2d 100644 --- a/web-app/src/services/__tests__/models.test.ts +++ b/web-app/src/services/__tests__/models.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { DefaultModelsService } from '../models/default' import type { HuggingFaceRepo, CatalogModel } from '../models/types' -import { EngineManager, Model } from '@janhq/core' +import { EngineManager } from '@janhq/core' // Mock EngineManager vi.mock('@janhq/core', () => ({ @@ -131,18 +131,19 @@ describe('DefaultModelsService', () => { expect(mockEngine.update).not.toHaveBeenCalled() }) - it('should update model when modelId differs from model.id', async () => { + it('should handle model when modelId differs from model.id', async () => { const modelId = 'old-model-id' const model = { id: 'new-model-id', settings: [{ key: 'temperature', value: 0.7 }], } - mockEngine.update.mockResolvedValue(undefined) await modelsService.updateModel(modelId, model as any) expect(mockEngine.updateSettings).toHaveBeenCalledWith(model.settings) - expect(mockEngine.update).toHaveBeenCalledWith(modelId, model) + // Note: Model ID updates are now handled at the provider level in the frontend + // The engine no longer has an update method for model metadata + expect(mockEngine.update).not.toHaveBeenCalled() }) }) diff --git a/web-app/src/services/__tests__/serviceHub.integration.test.ts b/web-app/src/services/__tests__/serviceHub.integration.test.ts index 5b89347a1..8a8a10344 100644 --- a/web-app/src/services/__tests__/serviceHub.integration.test.ts +++ b/web-app/src/services/__tests__/serviceHub.integration.test.ts @@ -213,4 +213,4 @@ describe('ServiceHub Integration Tests', () => { }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/web-specific.test.ts b/web-app/src/services/__tests__/web-specific.test.ts index 135b48c1c..52e544bf5 100644 --- a/web-app/src/services/__tests__/web-specific.test.ts +++ b/web-app/src/services/__tests__/web-specific.test.ts @@ -99,4 +99,4 @@ describe('Web-Specific Service Tests', () => { expect(typeof service.factoryReset).toBe('function') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/analytic/default.ts b/web-app/src/services/analytic/default.ts index eff3a14c3..e0ae67224 100644 --- a/web-app/src/services/analytic/default.ts +++ b/web-app/src/services/analytic/default.ts @@ -20,4 +20,4 @@ export class DefaultAnalyticService implements AnalyticService { await window.core?.api?.getAppConfigurations() return appConfiguration.distinct_id } -} \ No newline at end of file +} diff --git a/web-app/src/services/analytic/types.ts b/web-app/src/services/analytic/types.ts index e54e74424..b8f2fc6d7 100644 --- a/web-app/src/services/analytic/types.ts +++ b/web-app/src/services/analytic/types.ts @@ -5,4 +5,4 @@ export interface AnalyticService { updateDistinctId(id: string): Promise getAppDistinctId(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/app/default.ts b/web-app/src/services/app/default.ts index 9e54c6791..68fe1f0ea 100644 --- a/web-app/src/services/app/default.ts +++ b/web-app/src/services/app/default.ts @@ -39,4 +39,4 @@ export class DefaultAppService implements AppService { console.log('readYaml called with path:', path) throw new Error('readYaml not implemented in default app service') } -} \ No newline at end of file +} diff --git a/web-app/src/services/app/tauri.ts b/web-app/src/services/app/tauri.ts index b59a9f676..af770e93d 100644 --- a/web-app/src/services/app/tauri.ts +++ b/web-app/src/services/app/tauri.ts @@ -75,4 +75,4 @@ export class TauriAppService extends DefaultAppService { async readYaml(path: string): Promise { return await invoke('read_yaml', { path }) } -} \ No newline at end of file +} diff --git a/web-app/src/services/app/types.ts b/web-app/src/services/app/types.ts index 9b0c25b7e..4be49c584 100644 --- a/web-app/src/services/app/types.ts +++ b/web-app/src/services/app/types.ts @@ -17,4 +17,4 @@ export interface AppService { relocateJanDataFolder(path: string): Promise getServerStatus(): Promise readYaml(path: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/app/web.ts b/web-app/src/services/app/web.ts index 06ba65080..5702079d1 100644 --- a/web-app/src/services/app/web.ts +++ b/web-app/src/services/app/web.ts @@ -45,4 +45,4 @@ export class WebAppService implements AppService { console.log('YAML reading not available in web mode') throw new Error('readYaml not implemented in web app service') } -} \ No newline at end of file +} diff --git a/web-app/src/services/assistants/default.ts b/web-app/src/services/assistants/default.ts index 65d3cc58f..b74582510 100644 --- a/web-app/src/services/assistants/default.ts +++ b/web-app/src/services/assistants/default.ts @@ -31,4 +31,4 @@ export class DefaultAssistantsService implements AssistantsService { .get(ExtensionTypeEnum.Assistant) ?.deleteAssistant(assistant) } -} \ No newline at end of file +} diff --git a/web-app/src/services/assistants/types.ts b/web-app/src/services/assistants/types.ts index 1be730fe2..be43cfb49 100644 --- a/web-app/src/services/assistants/types.ts +++ b/web-app/src/services/assistants/types.ts @@ -8,4 +8,4 @@ export interface AssistantsService { getAssistants(): Promise createAssistant(assistant: Assistant): Promise deleteAssistant(assistant: Assistant): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/core/default.ts b/web-app/src/services/core/default.ts index 235e38294..f019e235d 100644 --- a/web-app/src/services/core/default.ts +++ b/web-app/src/services/core/default.ts @@ -38,4 +38,4 @@ export class DefaultCoreService implements CoreService { async getAppToken(): Promise { return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/core/tauri.ts b/web-app/src/services/core/tauri.ts index 8f83b0b2c..8b968510d 100644 --- a/web-app/src/services/core/tauri.ts +++ b/web-app/src/services/core/tauri.ts @@ -73,4 +73,4 @@ export class TauriCoreService extends DefaultCoreService { return null } } -} \ No newline at end of file +} diff --git a/web-app/src/services/core/types.ts b/web-app/src/services/core/types.ts index 8f518ffa4..6bcfb2983 100644 --- a/web-app/src/services/core/types.ts +++ b/web-app/src/services/core/types.ts @@ -21,4 +21,4 @@ export interface CoreService { // App token getAppToken(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/core/web.ts b/web-app/src/services/core/web.ts index 39a248611..abb2c6330 100644 --- a/web-app/src/services/core/web.ts +++ b/web-app/src/services/core/web.ts @@ -89,4 +89,4 @@ export class WebCoreService implements CoreService { console.warn('App token not available in web environment') return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/default.ts b/web-app/src/services/deeplink/default.ts index a7f8cf5da..27971ce8a 100644 --- a/web-app/src/services/deeplink/default.ts +++ b/web-app/src/services/deeplink/default.ts @@ -15,4 +15,4 @@ export class DefaultDeepLinkService implements DeepLinkService { async getCurrent(): Promise { return [] } -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/tauri.ts b/web-app/src/services/deeplink/tauri.ts index cab694353..722e5d209 100644 --- a/web-app/src/services/deeplink/tauri.ts +++ b/web-app/src/services/deeplink/tauri.ts @@ -24,4 +24,4 @@ export class TauriDeepLinkService extends DefaultDeepLinkService { return [] } } -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/types.ts b/web-app/src/services/deeplink/types.ts index 19b3ff517..22ecd5426 100644 --- a/web-app/src/services/deeplink/types.ts +++ b/web-app/src/services/deeplink/types.ts @@ -6,4 +6,4 @@ export interface DeepLinkService { onOpenUrl(handler: (urls: string[]) => void): Promise<() => void> getCurrent(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/web.ts b/web-app/src/services/deeplink/web.ts index bba92c43c..1c8df40be 100644 --- a/web-app/src/services/deeplink/web.ts +++ b/web-app/src/services/deeplink/web.ts @@ -24,4 +24,4 @@ export class WebDeepLinkService implements DeepLinkService { // Return current URL return [window.location.href] } -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/default.ts b/web-app/src/services/dialog/default.ts index 3232fd638..f2ec6d1d5 100644 --- a/web-app/src/services/dialog/default.ts +++ b/web-app/src/services/dialog/default.ts @@ -14,4 +14,4 @@ export class DefaultDialogService implements DialogService { console.log('dialog.save called with options:', options) return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/tauri.ts b/web-app/src/services/dialog/tauri.ts index 1b4efad28..d6d5fc2f2 100644 --- a/web-app/src/services/dialog/tauri.ts +++ b/web-app/src/services/dialog/tauri.ts @@ -33,4 +33,4 @@ export class TauriDialogService extends DefaultDialogService { return null } } -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/types.ts b/web-app/src/services/dialog/types.ts index 245155c36..8f3c4e3ab 100644 --- a/web-app/src/services/dialog/types.ts +++ b/web-app/src/services/dialog/types.ts @@ -16,4 +16,4 @@ export interface DialogOpenOptions { export interface DialogService { open(options?: DialogOpenOptions): Promise save(options?: DialogOpenOptions): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/web.ts b/web-app/src/services/dialog/web.ts index c735cbf41..9dbfd503d 100644 --- a/web-app/src/services/dialog/web.ts +++ b/web-app/src/services/dialog/web.ts @@ -54,4 +54,4 @@ export class WebDialogService implements DialogService { console.warn('Save dialog not supported in web environment') return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/EventEmitter.ts b/web-app/src/services/events/EventEmitter.ts index bb9e57ebb..762ca5018 100644 --- a/web-app/src/services/events/EventEmitter.ts +++ b/web-app/src/services/events/EventEmitter.ts @@ -44,4 +44,4 @@ export class EventEmitter { handler(args) }) } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/default.ts b/web-app/src/services/events/default.ts index 5b5a67492..f7f9aa83d 100644 --- a/web-app/src/services/events/default.ts +++ b/web-app/src/services/events/default.ts @@ -16,4 +16,4 @@ export class DefaultEventsService implements EventsService { // No-op unlisten function } } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/tauri.ts b/web-app/src/services/events/tauri.ts index b15e1e338..c58b03781 100644 --- a/web-app/src/services/events/tauri.ts +++ b/web-app/src/services/events/tauri.ts @@ -27,4 +27,4 @@ export class TauriEventsService extends DefaultEventsService { return () => {} } } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/types.ts b/web-app/src/services/events/types.ts index e57641114..0b033efdc 100644 --- a/web-app/src/services/events/types.ts +++ b/web-app/src/services/events/types.ts @@ -13,4 +13,4 @@ export interface UnlistenFn { export interface EventsService { emit(event: string, payload?: T, options?: EventOptions): Promise listen(event: string, handler: (event: { payload: T }) => void, options?: EventOptions): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/events/web.ts b/web-app/src/services/events/web.ts index a14a6fe0d..055fd4571 100644 --- a/web-app/src/services/events/web.ts +++ b/web-app/src/services/events/web.ts @@ -32,4 +32,4 @@ export class WebEventsService implements EventsService { this.eventTarget.removeEventListener(event, eventListener) } } -} \ No newline at end of file +} diff --git a/web-app/src/services/hardware/default.ts b/web-app/src/services/hardware/default.ts index 250e56de9..5f4df438c 100644 --- a/web-app/src/services/hardware/default.ts +++ b/web-app/src/services/hardware/default.ts @@ -21,4 +21,4 @@ export class DefaultHardwareService implements HardwareService { console.log('setActiveGpus called with data:', data) // No-op - not implemented in default service } -} \ No newline at end of file +} diff --git a/web-app/src/services/hardware/tauri.ts b/web-app/src/services/hardware/tauri.ts index 458b3037b..efcd23c37 100644 --- a/web-app/src/services/hardware/tauri.ts +++ b/web-app/src/services/hardware/tauri.ts @@ -30,4 +30,4 @@ export class TauriHardwareService extends DefaultHardwareService { // TODO: llama.cpp extension should handle this console.log(data) } -} \ No newline at end of file +} diff --git a/web-app/src/services/hardware/types.ts b/web-app/src/services/hardware/types.ts index 026d616c6..3307298f6 100644 --- a/web-app/src/services/hardware/types.ts +++ b/web-app/src/services/hardware/types.ts @@ -21,4 +21,4 @@ export interface HardwareService { } // Re-export hardware types for convenience -export type { HardwareData, SystemUsage } \ No newline at end of file +export type { HardwareData, SystemUsage } diff --git a/web-app/src/services/mcp/default.ts b/web-app/src/services/mcp/default.ts index 1bcce285a..b9a2f832e 100644 --- a/web-app/src/services/mcp/default.ts +++ b/web-app/src/services/mcp/default.ts @@ -66,4 +66,4 @@ export class DefaultMCPService implements MCPService { console.log('deactivateMCPServer called with name:', name) // No-op - not implemented in default service } -} \ No newline at end of file +} diff --git a/web-app/src/services/mcp/tauri.ts b/web-app/src/services/mcp/tauri.ts index 697bbc500..244e002c3 100644 --- a/web-app/src/services/mcp/tauri.ts +++ b/web-app/src/services/mcp/tauri.ts @@ -75,4 +75,4 @@ export class TauriMCPService extends DefaultMCPService { async deactivateMCPServer(name: string): Promise { return await invoke('deactivate_mcp_server', { name }) } -} \ No newline at end of file +} diff --git a/web-app/src/services/mcp/types.ts b/web-app/src/services/mcp/types.ts index c551c61c5..eef8e1bf3 100644 --- a/web-app/src/services/mcp/types.ts +++ b/web-app/src/services/mcp/types.ts @@ -32,4 +32,4 @@ export interface MCPService { // MCP Server lifecycle management activateMCPServer(name: string, config: MCPServerConfig): Promise deactivateMCPServer(name: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/mcp/web.ts b/web-app/src/services/mcp/web.ts index 124de93fd..c23283951 100644 --- a/web-app/src/services/mcp/web.ts +++ b/web-app/src/services/mcp/web.ts @@ -276,4 +276,4 @@ export class WebMCPService implements MCPService { private generateCancellationToken(): string { return `mcp_cancel_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` } -} \ No newline at end of file +} diff --git a/web-app/src/services/messages/default.ts b/web-app/src/services/messages/default.ts index 9f3ca69c6..8dbd63181 100644 --- a/web-app/src/services/messages/default.ts +++ b/web-app/src/services/messages/default.ts @@ -8,10 +8,16 @@ import { ExtensionTypeEnum, ThreadMessage, } from '@janhq/core' +import { TEMPORARY_CHAT_ID } from '@/constants/chat' import type { MessagesService } from './types' export class DefaultMessagesService implements MessagesService { async fetchMessages(threadId: string): Promise { + // Don't fetch messages from server for temporary chat - it's local only + if (threadId === TEMPORARY_CHAT_ID) { + return [] + } + return ( ExtensionManager.getInstance() .get(ExtensionTypeEnum.Conversational) @@ -21,6 +27,11 @@ export class DefaultMessagesService implements MessagesService { } async createMessage(message: ThreadMessage): Promise { + // Don't create messages on server for temporary chat - it's local only + if (message.thread_id === TEMPORARY_CHAT_ID) { + return message + } + return ( ExtensionManager.getInstance() .get(ExtensionTypeEnum.Conversational) @@ -30,8 +41,13 @@ export class DefaultMessagesService implements MessagesService { } async deleteMessage(threadId: string, messageId: string): Promise { + // Don't delete messages on server for temporary chat - it's local only + if (threadId === TEMPORARY_CHAT_ID) { + return + } + await ExtensionManager.getInstance() .get(ExtensionTypeEnum.Conversational) ?.deleteMessage(threadId, messageId) } -} \ No newline at end of file +} diff --git a/web-app/src/services/messages/types.ts b/web-app/src/services/messages/types.ts index ad5ae72c8..731de06d0 100644 --- a/web-app/src/services/messages/types.ts +++ b/web-app/src/services/messages/types.ts @@ -8,4 +8,4 @@ export interface MessagesService { fetchMessages(threadId: string): Promise createMessage(message: ThreadMessage): Promise deleteMessage(threadId: string, messageId: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/models/default.ts b/web-app/src/services/models/default.ts index 39c80f551..746f869d1 100644 --- a/web-app/src/services/models/default.ts +++ b/web-app/src/services/models/default.ts @@ -163,15 +163,14 @@ export class DefaultModelsService implements ModelsService { } async updateModel(modelId: string, model: Partial): Promise { - if (model.settings) + if (model.settings) { this.getEngine()?.updateSettings( model.settings as SettingComponentProps[] ) - if (modelId !== model.id) { - await this.getEngine() - ?.update(modelId, model) - .then(() => console.log('Model updated successfully')) } + // Note: Model name/ID updates are handled at the provider level in the frontend + // The engine doesn't have an update method for model metadata + console.log('Model update request processed for modelId:', modelId) } async pullModel( diff --git a/web-app/src/services/opener/default.ts b/web-app/src/services/opener/default.ts index 287e927b8..d7f6f2722 100644 --- a/web-app/src/services/opener/default.ts +++ b/web-app/src/services/opener/default.ts @@ -9,4 +9,4 @@ export class DefaultOpenerService implements OpenerService { console.log('revealItemInDir called with path:', path) // No-op - not implemented in default service } -} \ No newline at end of file +} diff --git a/web-app/src/services/opener/tauri.ts b/web-app/src/services/opener/tauri.ts index 9c465e521..c47bf687a 100644 --- a/web-app/src/services/opener/tauri.ts +++ b/web-app/src/services/opener/tauri.ts @@ -14,4 +14,4 @@ export class TauriOpenerService extends DefaultOpenerService { throw error } } -} \ No newline at end of file +} diff --git a/web-app/src/services/opener/types.ts b/web-app/src/services/opener/types.ts index 21e0d17f0..1dd7bee87 100644 --- a/web-app/src/services/opener/types.ts +++ b/web-app/src/services/opener/types.ts @@ -5,4 +5,4 @@ export interface OpenerService { revealItemInDir(path: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/path/default.ts b/web-app/src/services/path/default.ts index 90ed46e82..cde56bf4a 100644 --- a/web-app/src/services/path/default.ts +++ b/web-app/src/services/path/default.ts @@ -28,4 +28,4 @@ export class DefaultPathService implements PathService { console.log('path.extname called with path:', path) return '' } -} \ No newline at end of file +} diff --git a/web-app/src/services/path/tauri.ts b/web-app/src/services/path/tauri.ts index 80b5808c9..618c15abd 100644 --- a/web-app/src/services/path/tauri.ts +++ b/web-app/src/services/path/tauri.ts @@ -55,4 +55,4 @@ export class TauriPathService extends DefaultPathService { return lastDot > lastSlash ? path.substring(lastDot) : '' } } -} \ No newline at end of file +} diff --git a/web-app/src/services/path/types.ts b/web-app/src/services/path/types.ts index 269adc1c9..5f5c9af13 100644 --- a/web-app/src/services/path/types.ts +++ b/web-app/src/services/path/types.ts @@ -9,4 +9,4 @@ export interface PathService { dirname(path: string): Promise basename(path: string): Promise extname(path: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/path/web.ts b/web-app/src/services/path/web.ts index 724acb130..e207fa01b 100644 --- a/web-app/src/services/path/web.ts +++ b/web-app/src/services/path/web.ts @@ -37,4 +37,4 @@ export class WebPathService implements PathService { if (lastDot === -1 || lastDot === 0) return '' return basename.substring(lastDot) } -} \ No newline at end of file +} diff --git a/web-app/src/services/providers/default.ts b/web-app/src/services/providers/default.ts index 241138d28..3143d203f 100644 --- a/web-app/src/services/providers/default.ts +++ b/web-app/src/services/providers/default.ts @@ -22,4 +22,4 @@ export class DefaultProvidersService implements ProvidersService { fetch(): typeof fetch { return fetch } -} \ No newline at end of file +} diff --git a/web-app/src/services/providers/types.ts b/web-app/src/services/providers/types.ts index 1c6d81d90..324058f78 100644 --- a/web-app/src/services/providers/types.ts +++ b/web-app/src/services/providers/types.ts @@ -7,4 +7,4 @@ export interface ProvidersService { fetchModelsFromProvider(provider: ModelProvider): Promise updateSettings(providerName: string, settings: ProviderSetting[]): Promise fetch(): typeof fetch -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/default.ts b/web-app/src/services/theme/default.ts index 421cc102e..8dc131be5 100644 --- a/web-app/src/services/theme/default.ts +++ b/web-app/src/services/theme/default.ts @@ -18,4 +18,4 @@ export class DefaultThemeService implements ThemeService { } } } -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/tauri.ts b/web-app/src/services/theme/tauri.ts index 0f2f1f64d..e05d60c74 100644 --- a/web-app/src/services/theme/tauri.ts +++ b/web-app/src/services/theme/tauri.ts @@ -24,4 +24,4 @@ export class TauriThemeService extends DefaultThemeService { } } } -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/types.ts b/web-app/src/services/theme/types.ts index abcf8fc44..21c208072 100644 --- a/web-app/src/services/theme/types.ts +++ b/web-app/src/services/theme/types.ts @@ -7,4 +7,4 @@ export type ThemeMode = 'light' | 'dark' | null export interface ThemeService { setTheme(theme: ThemeMode): Promise getCurrentWindow(): { setTheme: (theme: ThemeMode) => Promise } -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/web.ts b/web-app/src/services/theme/web.ts index 39b1ff903..cf6ce01cf 100644 --- a/web-app/src/services/theme/web.ts +++ b/web-app/src/services/theme/web.ts @@ -22,4 +22,4 @@ export class WebThemeService implements ThemeService { } } } -} \ No newline at end of file +} diff --git a/web-app/src/services/threads/default.ts b/web-app/src/services/threads/default.ts index 72c66841a..4c10af26f 100644 --- a/web-app/src/services/threads/default.ts +++ b/web-app/src/services/threads/default.ts @@ -6,6 +6,7 @@ import { defaultAssistant } from '@/hooks/useAssistant' import { ExtensionManager } from '@/lib/extension' import { ConversationalExtension, ExtensionTypeEnum } from '@janhq/core' import type { ThreadsService } from './types' +import { TEMPORARY_CHAT_ID } from '@/constants/chat' export class DefaultThreadsService implements ThreadsService { async fetchThreads(): Promise { @@ -16,7 +17,10 @@ export class DefaultThreadsService implements ThreadsService { .then((threads) => { if (!Array.isArray(threads)) return [] - return threads.map((e) => { + // Filter out temporary threads from the list + const filteredThreads = threads.filter((e) => e.id !== TEMPORARY_CHAT_ID) + + return filteredThreads.map((e) => { return { ...e, updated: @@ -47,6 +51,11 @@ export class DefaultThreadsService implements ThreadsService { } async createThread(thread: Thread): Promise { + // For temporary threads, bypass the conversational extension (in-memory only) + if (thread.id === TEMPORARY_CHAT_ID) { + return thread + } + return ( ExtensionManager.getInstance() .get(ExtensionTypeEnum.Conversational) @@ -82,6 +91,11 @@ export class DefaultThreadsService implements ThreadsService { } async updateThread(thread: Thread): Promise { + // For temporary threads, skip updating via conversational extension + if (thread.id === TEMPORARY_CHAT_ID) { + return + } + await ExtensionManager.getInstance() .get(ExtensionTypeEnum.Conversational) ?.modifyThread({ @@ -118,6 +132,11 @@ export class DefaultThreadsService implements ThreadsService { } async deleteThread(threadId: string): Promise { + // For temporary threads, skip deleting via conversational extension + if (threadId === TEMPORARY_CHAT_ID) { + return + } + await ExtensionManager.getInstance() .get(ExtensionTypeEnum.Conversational) ?.deleteThread(threadId) diff --git a/web-app/src/services/threads/types.ts b/web-app/src/services/threads/types.ts index d0ce195cc..9f739624f 100644 --- a/web-app/src/services/threads/types.ts +++ b/web-app/src/services/threads/types.ts @@ -7,4 +7,4 @@ export interface ThreadsService { createThread(thread: Thread): Promise updateThread(thread: Thread): Promise deleteThread(threadId: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/updater/default.ts b/web-app/src/services/updater/default.ts index b648c5622..c0ba14aa2 100644 --- a/web-app/src/services/updater/default.ts +++ b/web-app/src/services/updater/default.ts @@ -19,4 +19,4 @@ export class DefaultUpdaterService implements UpdaterService { console.log('downloadAndInstallWithProgress called with callback:', typeof progressCallback) // No-op for non-Tauri platforms } -} \ No newline at end of file +} diff --git a/web-app/src/services/updater/tauri.ts b/web-app/src/services/updater/tauri.ts index 1db1ad294..a84951e7e 100644 --- a/web-app/src/services/updater/tauri.ts +++ b/web-app/src/services/updater/tauri.ts @@ -60,4 +60,4 @@ export class TauriUpdaterService extends DefaultUpdaterService { throw error } } -} \ No newline at end of file +} diff --git a/web-app/src/services/updater/types.ts b/web-app/src/services/updater/types.ts index c61642666..5dceee89e 100644 --- a/web-app/src/services/updater/types.ts +++ b/web-app/src/services/updater/types.ts @@ -24,4 +24,4 @@ export interface UpdaterService { downloadAndInstallWithProgress( progressCallback: (event: UpdateProgressEvent) => void ): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/window/default.ts b/web-app/src/services/window/default.ts index 08483743c..ea48a0173 100644 --- a/web-app/src/services/window/default.ts +++ b/web-app/src/services/window/default.ts @@ -40,4 +40,4 @@ export class DefaultWindowService implements WindowService { async openLocalApiServerLogsWindow(): Promise { // No-op } -} \ No newline at end of file +} diff --git a/web-app/src/services/window/tauri.ts b/web-app/src/services/window/tauri.ts index 56c038425..a6dd643c7 100644 --- a/web-app/src/services/window/tauri.ts +++ b/web-app/src/services/window/tauri.ts @@ -139,4 +139,4 @@ export class TauriWindowService extends DefaultWindowService { throw error } } -} \ No newline at end of file +} diff --git a/web-app/src/services/window/types.ts b/web-app/src/services/window/types.ts index 029f008aa..2f109427c 100644 --- a/web-app/src/services/window/types.ts +++ b/web-app/src/services/window/types.ts @@ -32,4 +32,4 @@ export interface WindowService { openLogsWindow(): Promise openSystemMonitorWindow(): Promise openLocalApiServerLogsWindow(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/window/web.ts b/web-app/src/services/window/web.ts index 8cc01b8cb..da49077a1 100644 --- a/web-app/src/services/window/web.ts +++ b/web-app/src/services/window/web.ts @@ -61,4 +61,4 @@ export class WebWindowService implements WindowService { async openLocalApiServerLogsWindow(): Promise { console.warn('Cannot open local API server logs window in web environment') } -} \ No newline at end of file +} diff --git a/web-app/src/test/mocks/extensions-web.ts b/web-app/src/test/mocks/extensions-web.ts index 908f56c90..0748e905f 100644 --- a/web-app/src/test/mocks/extensions-web.ts +++ b/web-app/src/test/mocks/extensions-web.ts @@ -18,4 +18,4 @@ export class ConversationalExtensionWeb { export default {} // Export registry type for TypeScript compatibility -export type WebExtensionRegistry = Record \ No newline at end of file +export type WebExtensionRegistry = Record diff --git a/web-app/src/test/setup.ts b/web-app/src/test/setup.ts index 9fde8b66b..b2286c2f3 100644 --- a/web-app/src/test/setup.ts +++ b/web-app/src/test/setup.ts @@ -207,4 +207,4 @@ Object.defineProperty(window, 'matchMedia', { // runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { cleanup() -}) \ No newline at end of file +}) diff --git a/web-app/src/types/modelProviders.d.ts b/web-app/src/types/modelProviders.d.ts index eb035e471..93cdd0df2 100644 --- a/web-app/src/types/modelProviders.d.ts +++ b/web-app/src/types/modelProviders.d.ts @@ -28,6 +28,7 @@ type Model = { id: string model?: string name?: string + displayName?: string version?: number | string description?: string format?: string diff --git a/web-app/src/utils/__tests__/error.test.ts b/web-app/src/utils/__tests__/error.test.ts index e6286060c..07efd387c 100644 --- a/web-app/src/utils/__tests__/error.test.ts +++ b/web-app/src/utils/__tests__/error.test.ts @@ -11,4 +11,4 @@ describe('error utilities', () => { expect(typeof OUT_OF_CONTEXT_SIZE).toBe('string') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/formatDate.test.ts b/web-app/src/utils/__tests__/formatDate.test.ts index 296d95da6..9d559f66f 100644 --- a/web-app/src/utils/__tests__/formatDate.test.ts +++ b/web-app/src/utils/__tests__/formatDate.test.ts @@ -100,4 +100,4 @@ describe('formatDate', () => { expect(formatted).not.toMatch(/\d{1,2}:\d{2}/i) expect(formatted).not.toMatch(/(AM|PM)/i) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/highlight.test.ts b/web-app/src/utils/__tests__/highlight.test.ts index 0277ba41a..0379c60a1 100644 --- a/web-app/src/utils/__tests__/highlight.test.ts +++ b/web-app/src/utils/__tests__/highlight.test.ts @@ -68,4 +68,4 @@ describe('highlight utility', () => { expect(result2).toBe('Hello World') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/number.test.ts b/web-app/src/utils/__tests__/number.test.ts index ad5848f3c..c31ba9d09 100644 --- a/web-app/src/utils/__tests__/number.test.ts +++ b/web-app/src/utils/__tests__/number.test.ts @@ -66,4 +66,4 @@ describe('toNumber', () => { expect(toNumber('\t42\n')).toBe(42) expect(toNumber('\r\n -5.5 \t')).toBe(-5.5) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/reasoning.test.ts b/web-app/src/utils/__tests__/reasoning.test.ts index f9717b753..76c7853d9 100644 --- a/web-app/src/utils/__tests__/reasoning.test.ts +++ b/web-app/src/utils/__tests__/reasoning.test.ts @@ -378,4 +378,4 @@ describe('ReasoningProcessor', () => { expect(result3).toBe('Second thought') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/teamEmoji.test.ts b/web-app/src/utils/__tests__/teamEmoji.test.ts index eda023c01..2163f9cdf 100644 --- a/web-app/src/utils/__tests__/teamEmoji.test.ts +++ b/web-app/src/utils/__tests__/teamEmoji.test.ts @@ -39,4 +39,4 @@ describe('teamEmoji utility', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/highlight.ts b/web-app/src/utils/highlight.ts index fe7cedcef..282ca9aa6 100644 --- a/web-app/src/utils/highlight.ts +++ b/web-app/src/utils/highlight.ts @@ -38,4 +38,4 @@ export function highlightFzfMatch(text: string, positions: number[], highlightCl : part.text ) .join(''); -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 2cb509689..ce900004b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3453,45 +3453,40 @@ __metadata: languageName: node linkType: hard -"@jan/extensions-web@link:../extensions-web::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app": - version: 0.0.0-use.local - resolution: "@jan/extensions-web@link:../extensions-web::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app" - languageName: node - linkType: soft - -"@jan/extensions-web@workspace:extensions-web": +"@jan/extensions-web@workspace:*, @jan/extensions-web@workspace:extensions-web": version: 0.0.0-use.local resolution: "@jan/extensions-web@workspace:extensions-web" dependencies: "@janhq/core": "workspace:*" "@modelcontextprotocol/sdk": "npm:1.17.5" + "@tabler/icons-react": "npm:^3.34.0" + "@types/react": "npm:19.1.2" + react: "npm:19.0.0" typescript: "npm:5.9.2" vite: "npm:5.4.20" vitest: "npm:2.1.9" zustand: "npm:5.0.8" peerDependencies: "@janhq/core": "*" + "@tabler/icons-react": "*" + react: 19.0.0 zustand: 5.0.3 languageName: unknown linkType: soft -"@janhq/core@link:../core::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app": - version: 0.0.0-use.local - resolution: "@janhq/core@link:../core::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app" - languageName: node - linkType: soft - "@janhq/core@workspace:*, @janhq/core@workspace:core": version: 0.0.0-use.local resolution: "@janhq/core@workspace:core" dependencies: "@npmcli/arborist": "npm:^7.1.0" "@types/node": "npm:^22.10.0" + "@types/react": "npm:19.1.2" "@vitest/coverage-v8": "npm:^2.1.8" "@vitest/ui": "npm:^2.1.8" eslint: "npm:8.57.0" happy-dom: "npm:^15.11.6" pacote: "npm:^21.0.0" + react: "npm:19.0.0" request: "npm:^2.88.2" request-progress: "npm:^3.0.0" rimraf: "npm:^6.0.1" @@ -3501,6 +3496,8 @@ __metadata: typescript: "npm:^5.8.3" ulidx: "npm:^2.3.0" vitest: "npm:^2.1.8" + peerDependencies: + react: 19.0.0 languageName: unknown linkType: soft @@ -3512,8 +3509,8 @@ __metadata: "@dnd-kit/modifiers": "npm:9.0.0" "@dnd-kit/sortable": "npm:10.0.0" "@eslint/js": "npm:8.57.0" - "@jan/extensions-web": "link:../extensions-web" - "@janhq/core": "link:../core" + "@jan/extensions-web": "workspace:*" + "@janhq/core": "workspace:*" "@radix-ui/react-accordion": "npm:1.2.11" "@radix-ui/react-avatar": "npm:1.1.10" "@radix-ui/react-dialog": "npm:1.1.15" @@ -7022,6 +7019,17 @@ __metadata: languageName: node linkType: hard +"@tabler/icons-react@npm:^3.34.0": + version: 3.35.0 + resolution: "@tabler/icons-react@npm:3.35.0" + dependencies: + "@tabler/icons": "npm:3.35.0" + peerDependencies: + react: ">= 16" + checksum: 10c0/8d280fcdae00916b001142ba0800ea05d8fa2acdcbd82f88a299b4141fb941237be2e826b86b1af710e038b4f8bb6f76f452c3309c29fd62398b4d5789c2b3e0 + languageName: node + linkType: hard + "@tabler/icons@npm:3.34.0": version: 3.34.0 resolution: "@tabler/icons@npm:3.34.0" @@ -7029,6 +7037,13 @@ __metadata: languageName: node linkType: hard +"@tabler/icons@npm:3.35.0": + version: 3.35.0 + resolution: "@tabler/icons@npm:3.35.0" + checksum: 10c0/93098828128ffed2cf412b39bd78992f93f25b22349a4e04523d2a018b7fe376ddeff105babcc3efedd707aa00b705425c7d9f598d6987552a563c62125795a2 + languageName: node + linkType: hard + "@tailwindcss/node@npm:4.1.4": version: 4.1.4 resolution: "@tailwindcss/node@npm:4.1.4"