From 3660fb8bdd333b9ab135b55c35e85ae4f9e48993 Mon Sep 17 00:00:00 2001 From: TamHC <79314879+TamHC@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:30:38 +0800 Subject: [PATCH 1/8] Update suggestionGeneratorAgent.ts match the suggestions to user's language --- src/lib/chains/suggestionGeneratorAgent.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/chains/suggestionGeneratorAgent.ts b/src/lib/chains/suggestionGeneratorAgent.ts index 9129059..dd49416 100644 --- a/src/lib/chains/suggestionGeneratorAgent.ts +++ b/src/lib/chains/suggestionGeneratorAgent.ts @@ -8,11 +8,14 @@ import { ChatOpenAI } from '@langchain/openai'; const suggestionGeneratorPrompt = ` You are an AI suggestion generator for an AI powered search engine. You will be given a conversation below. You need to generate 4-5 suggestions based on the conversation. The suggestion should be relevant to the conversation that can be used by the user to ask the chat model for more information. + You need to make sure the suggestions are relevant to the conversation and are helpful to the user. Keep a note that the user might use these suggestions to ask a chat model for more information. + Make sure the suggestions are medium in length and are informative and relevant to the conversation. -Provide these suggestions separated by newlines between the XML tags and . For example: +**Important: Generate the suggestions in the same language as the user's conversation. If the conversation is in English, provide suggestions in English. If the conversation is in Chinese, provide suggestions in Chinese. If the conversation is in Spanish, provide suggestions in Spanish, etc. Match the language, dialect, and tone of the user's input.** +Provide these suggestions separated by newlines between the XML tags and . For example: Tell me more about SpaceX and their recent projects What is the latest news on SpaceX? From 26952ff6c8628f0ecdb99b7e47312c7ebe55d1b3 Mon Sep 17 00:00:00 2001 From: TamHC <79314879+TamHC@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:35:41 +0800 Subject: [PATCH 2/8] Add files via upload --- src/components/ChatWindow.tsx | 4 ++ src/components/EmptyChat.tsx | 10 ++-- src/components/IncognitoToggle.tsx | 84 ++++++++++++++++++++++++++++++ src/components/Navbar.tsx | 2 + 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/components/IncognitoToggle.tsx diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 93c8a0c..ea29b1e 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -334,6 +334,9 @@ const ChatWindow = ({ id }: { id?: string }) => { return; } + // 檢查無痕模式 + const isIncognito = localStorage.getItem('incognitoMode') === 'true'; + setLoading(true); setMessageAppeared(false); @@ -481,6 +484,7 @@ const ChatWindow = ({ id }: { id?: string }) => { provider: embeddingModelProvider.provider, }, systemInstructions: localStorage.getItem('systemInstructions'), + isIncognito: isIncognito, }), }); diff --git a/src/components/EmptyChat.tsx b/src/components/EmptyChat.tsx index 42e5a09..28b7b91 100644 --- a/src/components/EmptyChat.tsx +++ b/src/components/EmptyChat.tsx @@ -4,6 +4,7 @@ import { File } from './ChatWindow'; import Link from 'next/link'; import WeatherWidget from './WeatherWidget'; import NewsArticleWidget from './NewsArticleWidget'; +import IncognitoToggle from './IncognitoToggle'; const EmptyChat = ({ sendMessage, @@ -35,9 +36,12 @@ const EmptyChat = ({
-

- Research begins here. -

+
+

+ Research begins here. +

+ +
{ + const [isIncognito, setIsIncognito] = useState(false); + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + // 初始化無痕模式狀態 + useEffect(() => { + // 檢查URL參數 + const incognitoParam = searchParams.get('incognito'); + if (incognitoParam !== null) { + const incognitoValue = incognitoParam === 'true'; + setIsIncognito(incognitoValue); + localStorage.setItem('incognitoMode', incognitoValue.toString()); + return; + } + + // 檢查localStorage + const savedIncognito = localStorage.getItem('incognitoMode'); + if (savedIncognito !== null) { + setIsIncognito(savedIncognito === 'true'); + } + }, [searchParams]); + + const toggleIncognito = () => { + const newIncognitoState = !isIncognito; + setIsIncognito(newIncognitoState); + + // 保存到localStorage + localStorage.setItem('incognitoMode', newIncognitoState.toString()); + + // 更新URL參數 + const params = new URLSearchParams(searchParams.toString()); + if (newIncognitoState) { + params.set('incognito', 'true'); + } else { + params.delete('incognito'); + } + + const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname; + router.replace(newUrl, { scroll: false }); + + // 觸發自定義事件,通知其他組件無痕模式狀態變化 + window.dispatchEvent(new CustomEvent('incognitoModeChanged', { + detail: { isIncognito: newIncognitoState } + })); + }; + + return ( + + ); +}; + +export default IncognitoToggle; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index e406ade..d24b3fc 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -10,6 +10,7 @@ import { Transition, } from '@headlessui/react'; import jsPDF from 'jspdf'; +import IncognitoToggle from './IncognitoToggle'; const downloadFile = (filename: string, content: string, type: string) => { const blob = new Blob([content], { type }); @@ -173,6 +174,7 @@ const Navbar = ({

{title}

+ From 883e4570095f1d35718d4fe558bed79b5fa1e54d Mon Sep 17 00:00:00 2001 From: TamHC <79314879+TamHC@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:26:18 +0800 Subject: [PATCH 3/8] implemented the history retention feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 1. __History Retention Configuration__ - __config.toml__: Added `[HISTORY]` section with `RETENTION_DAYS = 30` setting - __Backend Integration__: Updated configuration handling to support history retention - __API Endpoints__: Modified `/api/config` to read/write history retention settings ### 2. __User Interface__ - __Settings Page__: Added "History Settings" section with number input for retention days - __Real-time Updates__: Settings are saved to config.toml when changed - __Clear Documentation__: Explains that retention only applies when incognito mode is off ### 3. __Automatic History Cleanup__ - __Background Processing__: Cleanup runs automatically when new chats are created (non-incognito mode) - __Smart Logic__: Only deletes chats older than configured retention period - __Complete Cleanup__: Removes both chat records and associated messages - __Performance Optimized__: Non-blocking background execution ### 4. __Manual Cleanup API__ - __Endpoint__: `POST /api/cleanup-history` for manual cleanup triggers - __Utility Functions__: Reusable cleanup logic in dedicated utility file ### 5. __Docker Rebuild__ - __Container Rebuild__: Successfully rebuilt the Docker containers with new features - __Configuration Persistence__: config.toml changes are preserved in Docker volume - __Application Ready__: The application should now be accessible at [](http://localhost:3000) ## Key Features: 1. __Incognito Mode Integration__: History retention only applies when incognito mode is OFF 2. __Flexible Configuration__: 0 = keep forever, any positive number = days to retain 3. __Automatic Cleanup__: Runs in background when creating new chats 4. __Manual Control__: API endpoint for manual cleanup triggers 5. __Database Integrity__: Properly removes both chats and associated messages ## Testing the Feature: 1. __Access the Application__: Open [](http://localhost:3000) in your browser 2. __Configure Settings__: Go to Settings → History Settings → Set retention days 3. __Test Incognito Mode__: Toggle incognito mode on/off to see different behaviors 4. __Create Test Chats__: Create chats in both modes to verify functionality 5. __Manual Cleanup__: Use the `/api/cleanup-history` endpoint to test manual cleanup --- src/app/api/chat/route.ts | 43 ++++++++++++------ src/app/api/cleanup-history/route.ts | 19 ++++++++ src/app/api/config/route.ts | 5 +++ src/app/settings/page.tsx | 28 ++++++++++++ src/lib/config.ts | 5 +++ src/lib/utils/historyCleanup.ts | 66 ++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 src/app/api/cleanup-history/route.ts create mode 100644 src/lib/utils/historyCleanup.ts diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index e566edb..31e5550 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -21,6 +21,7 @@ import { getCustomOpenaiModelName, } from '@/lib/config'; import { searchHandlers } from '@/lib/search'; +import { cleanupOldHistory } from '@/lib/utils/historyCleanup'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -50,6 +51,7 @@ type Body = { chatModel: ChatModel; embeddingModel: EmbeddingModel; systemInstructions: string; + isIncognito?: boolean; }; const handleEmitterEvents = async ( @@ -58,6 +60,7 @@ const handleEmitterEvents = async ( encoder: TextEncoder, aiMessageId: string, chatId: string, + isIncognito: boolean = false, ) => { let recievedMessage = ''; let sources: any[] = []; @@ -101,18 +104,21 @@ const handleEmitterEvents = async ( ); writer.close(); - db.insert(messagesSchema) - .values({ - content: recievedMessage, - chatId: chatId, - messageId: aiMessageId, - role: 'assistant', - metadata: JSON.stringify({ - createdAt: new Date(), - ...(sources && sources.length > 0 && { sources }), - }), - }) - .execute(); + // 在無痕模式下不保存助手回應到數據庫 + if (!isIncognito) { + db.insert(messagesSchema) + .values({ + content: recievedMessage, + chatId: chatId, + messageId: aiMessageId, + role: 'assistant', + metadata: JSON.stringify({ + createdAt: new Date(), + ...(sources && sources.length > 0 && { sources }), + }), + }) + .execute(); + } }); stream.on('error', (data) => { const parsedData = JSON.parse(data); @@ -149,6 +155,11 @@ const handleHistorySave = async ( files: files.map(getFileDetails), }) .execute(); + + // Trigger history cleanup for new chats (run in background) + cleanupOldHistory().catch(err => { + console.error('Background history cleanup failed:', err); + }); } const messageExists = await db.query.messages.findFirst({ @@ -286,8 +297,12 @@ export const POST = async (req: Request) => { const writer = responseStream.writable.getWriter(); const encoder = new TextEncoder(); - handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId); - handleHistorySave(message, humanMessageId, body.focusMode, body.files); + handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId, body.isIncognito); + + // 在無痕模式下不保存聊天記錄 + if (!body.isIncognito) { + handleHistorySave(message, humanMessageId, body.focusMode, body.files); + } return new Response(responseStream.readable, { headers: { diff --git a/src/app/api/cleanup-history/route.ts b/src/app/api/cleanup-history/route.ts new file mode 100644 index 0000000..435e940 --- /dev/null +++ b/src/app/api/cleanup-history/route.ts @@ -0,0 +1,19 @@ +import { cleanupOldHistory } from '@/lib/utils/historyCleanup'; + +export const POST = async (req: Request) => { + try { + const result = await cleanupOldHistory(); + + return Response.json({ + message: result.message, + deletedChats: result.deletedChats + }, { status: 200 }); + + } catch (err) { + console.error('An error occurred while cleaning up history:', err); + return Response.json( + { message: 'An error occurred while cleaning up history' }, + { status: 500 }, + ); + } +}; diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts index c1e5bbd..bad18ae 100644 --- a/src/app/api/config/route.ts +++ b/src/app/api/config/route.ts @@ -9,6 +9,7 @@ import { getOpenaiApiKey, getDeepseekApiKey, getLMStudioApiEndpoint, + getHistoryRetentionDays, updateConfig, } from '@/lib/config'; import { @@ -60,6 +61,7 @@ export const GET = async (req: Request) => { config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl(); config['customOpenaiApiKey'] = getCustomOpenaiApiKey(); config['customOpenaiModelName'] = getCustomOpenaiModelName(); + config['historyRetentionDays'] = getHistoryRetentionDays(); return Response.json({ ...config }, { status: 200 }); } catch (err) { @@ -76,6 +78,9 @@ export const POST = async (req: Request) => { const config = await req.json(); const updatedConfig = { + HISTORY: { + RETENTION_DAYS: config.historyRetentionDays, + }, MODELS: { OPENAI: { API_KEY: config.openaiApiKey, diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 6f20f01..984af14 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -26,6 +26,7 @@ interface SettingsType { customOpenaiApiKey: string; customOpenaiApiUrl: string; customOpenaiModelName: string; + historyRetentionDays: number; } interface InputProps extends React.InputHTMLAttributes { @@ -512,6 +513,33 @@ const Page = () => {
+ +
+
+

+ History Retention (Days) +

+

+ Number of days to keep chat history when incognito mode is off (0 = keep forever) +

+ { + setConfig((prev) => ({ + ...prev!, + historyRetentionDays: parseInt(e.target.value) || 0, + })); + }} + onSave={(value) => saveConfig('historyRetentionDays', parseInt(value) || 0)} + /> +
+
+
+