- {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 93e27d640..1d0fc4932 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,16 +24,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)
@@ -52,9 +101,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(
@@ -140,9 +213,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)