From b69330ffcaaa83a98a4ef7adcd24bd5418814797 Mon Sep 17 00:00:00 2001 From: eckartal Date: Tue, 30 Sep 2025 11:06:23 +0800 Subject: [PATCH 1/3] Fix OG image paths and move images to general folder - Move OG images from _assets/ to public/assets/images/general/ - Update all blog post references to use correct paths - Remove duplicate images from _assets/ folder - Fix image paths in content to use /assets/images/general/ format - Update Twitter image references to match OG image paths Files updated: - chatgpt-alternatives.mdx - deepresearch.mdx - deepseek-r1-locally.mdx - how-we-benchmark-kernels.mdx - is-chatgpt-down-use-jan.mdx - offline-chatgpt-alternative.mdx - qwen3-settings.mdx - run-ai-models-locally.mdx - run-gpt-oss-locally.mdx --- .../images/general}/chatgpt-alternative-jan.jpeg | Bin .../images/general}/cover-kernel-benchmarking.png | Bin .../images/general}/deepseek-r1-locally-jan.jpg | Bin .../assets/images/general}/gpt-oss locally.jpeg | Bin .../assets/images/general}/is-chatgpt-down.jpg | Bin .../general}/offline-chatgpt-alternatives-jan.jpg | Bin .../assets/images/general/qwen3-30b-settings.jpg} | Bin .../images/general}/research-result-local.png | Bin .../images/general}/run-ai-locally-with-jan.jpg | Bin docs/src/pages/post/chatgpt-alternatives.mdx | 6 +++--- docs/src/pages/post/deepresearch.mdx | 4 ++-- docs/src/pages/post/deepseek-r1-locally.mdx | 4 ++-- docs/src/pages/post/how-we-benchmark-kernels.mdx | 2 +- docs/src/pages/post/is-chatgpt-down-use-jan.mdx | 10 +++++----- docs/src/pages/post/offline-chatgpt-alternative.mdx | 6 +++--- docs/src/pages/post/qwen3-settings.mdx | 2 +- docs/src/pages/post/run-ai-models-locally.mdx | 4 ++-- docs/src/pages/post/run-gpt-oss-locally.mdx | 6 ++---- 18 files changed, 21 insertions(+), 23 deletions(-) rename docs/{src/pages/post/_assets => public/assets/images/general}/chatgpt-alternative-jan.jpeg (100%) rename docs/{src/pages/post/_assets => public/assets/images/general}/cover-kernel-benchmarking.png (100%) rename docs/{src/pages/post/_assets => public/assets/images/general}/deepseek-r1-locally-jan.jpg (100%) rename docs/{src/pages/post/_assets => public/assets/images/general}/gpt-oss locally.jpeg (100%) rename docs/{src/pages/post/_assets => public/assets/images/general}/is-chatgpt-down.jpg (100%) rename docs/{src/pages/post/_assets => public/assets/images/general}/offline-chatgpt-alternatives-jan.jpg (100%) rename docs/{src/pages/post/_assets/qwen3-settings-jan-ai.jpeg => public/assets/images/general/qwen3-30b-settings.jpg} (100%) rename docs/{src/pages/post/_assets => public/assets/images/general}/research-result-local.png (100%) rename docs/{src/pages/post/_assets => public/assets/images/general}/run-ai-locally-with-jan.jpg (100%) diff --git a/docs/src/pages/post/_assets/chatgpt-alternative-jan.jpeg b/docs/public/assets/images/general/chatgpt-alternative-jan.jpeg similarity index 100% rename from docs/src/pages/post/_assets/chatgpt-alternative-jan.jpeg rename to docs/public/assets/images/general/chatgpt-alternative-jan.jpeg 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/src/pages/post/_assets/is-chatgpt-down.jpg b/docs/public/assets/images/general/is-chatgpt-down.jpg similarity index 100% rename from docs/src/pages/post/_assets/is-chatgpt-down.jpg rename to docs/public/assets/images/general/is-chatgpt-down.jpg 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/src/pages/post/chatgpt-alternatives.mdx b/docs/src/pages/post/chatgpt-alternatives.mdx index 208bda1bc..36f44e5c3 100644 --- a/docs/src/pages/post/chatgpt-alternatives.mdx +++ b/docs/src/pages/post/chatgpt-alternatives.mdx @@ -4,13 +4,13 @@ description: "See the best ChatGPT alternatives in 2025. We've listed tools that 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/chatgpt-alternative-jan.jpeg +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/chatgpt-alternative-jan.jpeg + image: assets/images/general/chatgpt-alternative-jan.jpeg --- import { Callout } from 'nextra/components' import CTABlog from '@/components/Blog/CTA' @@ -33,7 +33,7 @@ If you want options that fit different needs, offline use, privacy, or specializ ### Jan is the best ChatGPT alternative -![Use Jan to chat with AI models without internet access](./_assets/chatgpt-alternative-jan.jpeg) +![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: diff --git a/docs/src/pages/post/deepresearch.mdx b/docs/src/pages/post/deepresearch.mdx index f3f1c0ee7..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' 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 index bed499d19..f7e82cef8 100644 --- a/docs/src/pages/post/is-chatgpt-down-use-jan.mdx +++ b/docs/src/pages/post/is-chatgpt-down-use-jan.mdx @@ -4,13 +4,13 @@ description: "Check if ChatGPT down right now, and learn how to use AI that neve tags: AI, ChatGPT down, ChatGPT alternative, Jan, local AI, offline AI, ChatGPT at capacity categories: guides date: 2025-09-30 -ogImage: _assets/is-chatgpt-down.jpg +ogImage: assets/images/general/is-chatgpt-down.jpg twitter: card: summary_large_image site: "@jandotai" - title: "Realtime Status Checker: Is ChatGPT down?" + 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/is-chatgpt-down.jpg + image: assets/images/general/is-chatgpt-down.jpg --- import { Callout } from 'nextra/components' import CTABlog from '@/components/Blog/CTA' @@ -20,7 +20,7 @@ import { OpenAIStatusChecker } from '@/components/OpenAIStatusChecker' If you're seeing ChatGPT is down, it could a good signal to switch to [Jan](https://www.jan.ai/), AI that never goes down. -## 🔴 Realtime Status Checker: Is ChatGPT down? +## 🔴 Realtime Status: Is ChatGPT down? This live tracker shows if ChatGPT is down right now. @@ -66,7 +66,7 @@ This live tracker shows if ChatGPT is down right now. 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/chatgpt-alternative-jan.jpeg) +![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: diff --git a/docs/src/pages/post/offline-chatgpt-alternative.mdx b/docs/src/pages/post/offline-chatgpt-alternative.mdx index 6f16b0334..7f94cc23e 100644 --- a/docs/src/pages/post/offline-chatgpt-alternative.mdx +++ b/docs/src/pages/post/offline-chatgpt-alternative.mdx @@ -4,13 +4,13 @@ description: "Use offline AI with Jan, a free & open-source alternative to ChatG 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: "Use offline AI with Jan, a free & open-source alternative to ChatGPT that runs 100% offline." - image: _assets/offline-chatgpt-alternatives-jan.jpg + image: assets/images/general/offline-chatgpt-alternatives-jan.jpg --- import { Callout } from 'nextra/components' import CTABlog from '@/components/Blog/CTA' @@ -64,7 +64,7 @@ If you'd like to learn more about local AI, check [how to run AI models locally ### 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: 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? From 8b9aca27bf2f6658b0da44f8d95ec437ab26a6c8 Mon Sep 17 00:00:00 2001 From: eckartal Date: Tue, 30 Sep 2025 11:25:45 +0800 Subject: [PATCH 2/3] fix: remove Jan prefix from blog post titles for better SEO - Blog posts now use only frontmatter title without 'Jan -' prefix - Other pages maintain existing branding (Jan Desktop, Jan Server, Jan) - Improves SEO for blog content while preserving site branding --- docs/theme.config.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 ( From 2101242530f04c9f3cd961258c1fbbd409791c7b Mon Sep 17 00:00:00 2001 From: Dinh Long Nguyen Date: Tue, 30 Sep 2025 10:42:21 +0700 Subject: [PATCH 3/3] Feat: web temporary chat (#6650) * temporray chat stage1 * temporary page in root * temporary chat * handle redirection properly ` * temporary chat header * Update extensions-web/src/conversational-web/extension.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * update routetree * better error handling * fix strecthed assitant on desktop * update yarn link to workspace for better link consistency --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/conversational-web/extension.ts | 12 +++ extensions-web/src/jan-provider-web/api.ts | 77 +++++++++++----- .../src/jan-provider-web/provider.ts | 4 +- extensions-web/src/shared/auth/service.ts | 9 +- extensions-web/src/shared/types/errors.ts | 50 +++++++++++ web-app/package.json | 4 +- web-app/src/constants/chat.ts | 6 ++ web-app/src/containers/HeaderPage.tsx | 52 ++++++++++- web-app/src/hooks/useChat.ts | 28 +++++- web-app/src/hooks/useThreads.ts | 40 ++++++--- web-app/src/lib/platform/const.ts | 3 + web-app/src/lib/platform/types.ts | 3 + web-app/src/locales/en/chat.json | 2 + web-app/src/locales/en/common.json | 4 + web-app/src/routes/index.tsx | 22 +++-- web-app/src/routes/threads/$threadId.tsx | 87 ++++++++++++++++++- web-app/src/services/messages/default.ts | 16 ++++ web-app/src/services/threads/default.ts | 21 ++++- 18 files changed, 382 insertions(+), 58 deletions(-) create mode 100644 extensions-web/src/shared/types/errors.ts create mode 100644 web-app/src/constants/chat.ts 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..4c7bf1af6 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 { 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/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..7e6917faa --- /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 + } +} \ No newline at end of file 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/constants/chat.ts b/web-app/src/constants/chat.ts new file mode 100644 index 000000000..d147ca9e1 --- /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' \ No newline at end of file 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/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/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/lib/platform/const.ts b/web-app/src/lib/platform/const.ts index ab5a14e79..5621602b5 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(), + + // Temporary chat mode - enabled for web only + [PlatformFeature.TEMPORARY_CHAT]: !isPlatformTauri(), } \ No newline at end of file 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/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/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/threads/$threadId.tsx b/web-app/src/routes/threads/$threadId.tsx index 384cb764f..a8bd03d29 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' @@ -21,16 +23,63 @@ import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' import ScrollToBottom from '@/containers/ScrollToBottom' import { PromptProgress } from '@/components/PromptProgress' +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,9 +98,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( @@ -137,9 +210,15 @@ function ThreadDetail() {
- {PlatformFeatures[PlatformFeature.ASSISTANTS] && ( - - )} +
+ {PlatformFeatures[PlatformFeature.ASSISTANTS] && ( + + )} +
+
+ {threadId === TEMPORARY_CHAT_ID && } +
+
diff --git a/web-app/src/services/messages/default.ts b/web-app/src/services/messages/default.ts index 9f3ca69c6..177a09e29 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,6 +41,11 @@ 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) 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)