From 1b9efee52c7e16f3a7b58932aae3f53f0c0b2e28 Mon Sep 17 00:00:00 2001 From: Dinh Long Nguyen Date: Wed, 1 Oct 2025 22:47:38 +0700 Subject: [PATCH 1/2] feat: improve projects (#6698) * decouple successfully * only show movable projects for project items * handle delete covnersations when projects is removed * fix leftpanel assignemtn * fix lint --- web-app/src/containers/ChatInput.tsx | 28 +-- web-app/src/containers/LeftPanel.tsx | 21 +-- web-app/src/containers/ThreadList.tsx | 51 ++++-- .../dialogs/DeleteProjectDialog.tsx | 66 ++++++- web-app/src/hooks/useChat.ts | 24 ++- web-app/src/hooks/useThreadManagement.ts | 165 +++++++++++------- web-app/src/locales/en/common.json | 9 +- web-app/src/routes/project/$projectId.tsx | 2 +- web-app/src/routes/project/index.tsx | 21 +-- web-app/src/services/index.ts | 12 ++ web-app/src/services/projects/default.ts | 78 +++++++++ web-app/src/services/projects/types.ts | 42 +++++ web-app/src/services/projects/web.ts | 11 ++ web-app/src/services/threads/default.ts | 1 + 14 files changed, 380 insertions(+), 151 deletions(-) create mode 100644 web-app/src/services/projects/default.ts create mode 100644 web-app/src/services/projects/types.ts create mode 100644 web-app/src/services/projects/web.ts diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index cba580ebd..c96141876 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -4,7 +4,6 @@ import TextareaAutosize from 'react-textarea-autosize' import { cn } from '@/lib/utils' import { usePrompt } from '@/hooks/usePrompt' import { useThreads } from '@/hooks/useThreads' -import { useThreadManagement } from '@/hooks/useThreadManagement' import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { @@ -65,8 +64,6 @@ const ChatInput = ({ const prompt = usePrompt((state) => state.prompt) const setPrompt = usePrompt((state) => state.setPrompt) const currentThreadId = useThreads((state) => state.currentThreadId) - const updateThread = useThreads((state) => state.updateThread) - const { getFolderById } = useThreadManagement() const { t } = useTranslation() const spellCheckChatInput = useGeneralSetting( (state) => state.spellCheckChatInput @@ -183,31 +180,10 @@ const ChatInput = ({ sendMessage( prompt, true, - uploadedFiles.length > 0 ? uploadedFiles : undefined + uploadedFiles.length > 0 ? uploadedFiles : undefined, + projectId ) setUploadedFiles([]) - - // Handle project assignment for new threads - if (projectId && !currentThreadId) { - const project = getFolderById(projectId) - if (project) { - // Use setTimeout to ensure the thread is created first - setTimeout(() => { - const newCurrentThreadId = useThreads.getState().currentThreadId - if (newCurrentThreadId) { - updateThread(newCurrentThreadId, { - metadata: { - project: { - id: project.id, - name: project.name, - updated_at: project.updated_at, - }, - }, - }) - } - }, 100) - } - } } useEffect(() => { diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index f24dcec0d..0658bdd87 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -163,7 +163,7 @@ const LeftPanel = () => { const getFilteredThreads = useThreads((state) => state.getFilteredThreads) const threads = useThreads((state) => state.threads) - const { folders, addFolder, updateFolder, deleteFolder, getFolderById } = + const { folders, addFolder, updateFolder, getFolderById } = useThreadManagement() // Project dialog states @@ -204,19 +204,16 @@ const LeftPanel = () => { setDeleteProjectConfirmOpen(true) } - const confirmProjectDelete = () => { - if (deletingProjectId) { - deleteFolder(deletingProjectId) - setDeleteProjectConfirmOpen(false) - setDeletingProjectId(null) - } + const handleProjectDeleteClose = () => { + setDeleteProjectConfirmOpen(false) + setDeletingProjectId(null) } - const handleProjectSave = (name: string) => { + const handleProjectSave = async (name: string) => { if (editingProjectKey) { - updateFolder(editingProjectKey, name) + await updateFolder(editingProjectKey, name) } else { - const newProject = addFolder(name) + const newProject = await addFolder(name) // Navigate to the newly created project navigate({ to: '/project/$projectId', @@ -680,8 +677,8 @@ const LeftPanel = () => { /> { const { attributes, @@ -108,6 +110,18 @@ const SortableItem = memo( return (thread.title || '').replace(/]*>|<\/span>/g, '') }, [thread.title]) + const availableProjects = useMemo(() => { + return folders + .filter((f) => { + // Exclude the current project page we're on + if (f.id === currentProjectId) return false + // Exclude the project this thread is already assigned to + if (f.id === thread.metadata?.project?.id) return false + return true + }) + .sort((a, b) => b.updated_at - a.updated_at) + }, [folders, currentProjectId, thread.metadata?.project?.id]) + const assignThreadToProject = (threadId: string, projectId: string) => { const project = getFolderById(projectId) if (project && updateThread) { @@ -226,29 +240,27 @@ const SortableItem = memo( Add to project - {folders.length === 0 ? ( + {availableProjects.length === 0 ? ( No projects available ) : ( - folders - .sort((a, b) => b.updated_at - a.updated_at) - .map((folder) => ( - { - e.stopPropagation() - assignThreadToProject(thread.id, folder.id) - }} - > - - - {folder.name} - - - )) + availableProjects.map((folder) => ( + { + e.stopPropagation() + assignThreadToProject(thread.id, folder.id) + }} + > + + + {folder.name} + + + )) )} {thread.metadata?.project && ( <> @@ -296,9 +308,10 @@ type ThreadListProps = { isFavoriteSection?: boolean variant?: 'default' | 'project' showDate?: boolean + currentProjectId?: string } -function ThreadList({ threads, variant = 'default' }: ThreadListProps) { +function ThreadList({ threads, variant = 'default', currentProjectId }: ThreadListProps) { const sortedThreads = useMemo(() => { return threads.sort((a, b) => { return (b.updated || 0) - (a.updated || 0) @@ -322,7 +335,7 @@ function ThreadList({ threads, variant = 'default' }: ThreadListProps) { strategy={verticalListSortingStrategy} > {sortedThreads.map((thread, index) => ( - + ))} diff --git a/web-app/src/containers/dialogs/DeleteProjectDialog.tsx b/web-app/src/containers/dialogs/DeleteProjectDialog.tsx index f8c86a3b4..50379570d 100644 --- a/web-app/src/containers/dialogs/DeleteProjectDialog.tsx +++ b/web-app/src/containers/dialogs/DeleteProjectDialog.tsx @@ -1,4 +1,4 @@ -import { useRef } from 'react' +import { useRef, useMemo } from 'react' import { Dialog, DialogContent, @@ -10,26 +10,49 @@ import { import { Button } from '@/components/ui/button' import { toast } from 'sonner' import { useTranslation } from '@/i18n/react-i18next-compat' +import { useThreads } from '@/hooks/useThreads' +import { useThreadManagement } from '@/hooks/useThreadManagement' interface DeleteProjectDialogProps { open: boolean onOpenChange: (open: boolean) => void - onConfirm: () => void + projectId?: string projectName?: string } export function DeleteProjectDialog({ open, onOpenChange, - onConfirm, + projectId, projectName, }: DeleteProjectDialogProps) { const { t } = useTranslation() const deleteButtonRef = useRef(null) + const threads = useThreads((state) => state.threads) + const { deleteFolderWithThreads } = useThreadManagement() + + // Calculate thread stats for this project + const { threadCount, starredThreadCount } = useMemo(() => { + if (!projectId) return { threadCount: 0, starredThreadCount: 0 } + + const projectThreads = Object.values(threads).filter( + (thread) => thread.metadata?.project?.id === projectId + ) + const starredCount = projectThreads.filter( + (thread) => thread.isFavorite + ).length + + return { + threadCount: projectThreads.length, + starredThreadCount: starredCount, + } + }, [projectId, threads]) + + const handleConfirm = async () => { + if (!projectId) return - const handleConfirm = () => { try { - onConfirm() + await deleteFolderWithThreads(projectId) toast.success( projectName ? t('projects.deleteProjectDialog.successWithName', { projectName }) @@ -42,12 +65,15 @@ export function DeleteProjectDialog({ } } - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = async (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - handleConfirm() + await handleConfirm() } } + const hasStarredThreads = starredThreadCount > 0 + const hasThreads = threadCount > 0 + return ( {t('projects.deleteProjectDialog.title')} - - {t('projects.deleteProjectDialog.description')} + + {hasStarredThreads ? ( + <> +

+ {t('projects.deleteProjectDialog.starredWarning')} +

+

+ {t('projects.deleteProjectDialog.permanentDeleteWarning')} +

+ + ) : hasThreads ? ( +

+ {t('projects.deleteProjectDialog.permanentDelete')} +

+ ) : ( +

+ {t('projects.deleteProjectDialog.deleteEmptyProject', { projectName })} +

+ )} + {hasThreads && ( +

+ {t('projects.deleteProjectDialog.saveThreadsAdvice')} +

+ )}
diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 516a61b20..b4da700d2 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -83,7 +83,7 @@ export const useChat = () => { const setModelLoadError = useModelLoad((state) => state.setModelLoadError) const router = useRouter() - const getCurrentThread = useCallback(async () => { + const getCurrentThread = useCallback(async (projectId?: string) => { let currentThread = retrieveThread() if (!currentThread) { @@ -93,13 +93,28 @@ export const useChat = () => { const assistants = useAssistant.getState().assistants const selectedModel = useModelProvider.getState().selectedModel const selectedProvider = useModelProvider.getState().selectedProvider + + // Get project metadata if projectId is provided + let projectMetadata: { id: string; name: string; updated_at: number } | undefined + if (projectId) { + const project = await serviceHub.projects().getProjectById(projectId) + if (project) { + projectMetadata = { + id: project.id, + name: project.name, + updated_at: project.updated_at, + } + } + } + currentThread = await createThread( { id: selectedModel?.id ?? defaultModel(selectedProvider), provider: selectedProvider, }, currentPrompt, - assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] + assistants.find((a) => a.id === currentAssistant?.id) || assistants[0], + projectMetadata, ) router.navigate({ to: route.threadsDetail, @@ -221,9 +236,10 @@ export const useChat = () => { size: number base64: string dataUrl: string - }> + }>, + projectId?: string ) => { - const activeThread = await getCurrentThread() + const activeThread = await getCurrentThread(projectId) const selectedProvider = useModelProvider.getState().selectedProvider let activeProvider = getProviderByName(selectedProvider) diff --git a/web-app/src/hooks/useThreadManagement.ts b/web-app/src/hooks/useThreadManagement.ts index becb41def..86ce03991 100644 --- a/web-app/src/hooks/useThreadManagement.ts +++ b/web-app/src/hooks/useThreadManagement.ts @@ -1,83 +1,116 @@ import { create } from 'zustand' -import { persist, createJSONStorage } from 'zustand/middleware' -import { ulid } from 'ulidx' -import { localStorageKey } from '@/constants/localStorage' +import { getServiceHub } from '@/hooks/useServiceHub' import { useThreads } from '@/hooks/useThreads' - -type ThreadFolder = { - id: string - name: string - updated_at: number -} +import type { ThreadFolder } from '@/services/projects/types' +import { useEffect } from 'react' type ThreadManagementState = { folders: ThreadFolder[] setFolders: (folders: ThreadFolder[]) => void - addFolder: (name: string) => ThreadFolder - updateFolder: (id: string, name: string) => void - deleteFolder: (id: string) => void + addFolder: (name: string) => Promise + updateFolder: (id: string, name: string) => Promise + deleteFolder: (id: string) => Promise + deleteFolderWithThreads: (id: string) => Promise getFolderById: (id: string) => ThreadFolder | undefined + getProjectById: (id: string) => Promise } -export const useThreadManagement = create()( - persist( - (set, get) => ({ - folders: [], +const useThreadManagementStore = create()((set, get) => ({ + folders: [], - setFolders: (folders) => { - set({ folders }) - }, + setFolders: (folders) => { + set({ folders }) + }, - addFolder: (name) => { - const newFolder: ThreadFolder = { - id: ulid(), - name, - updated_at: Date.now(), - } - set((state) => ({ - folders: [...state.folders, newFolder], - })) - return newFolder - }, + addFolder: async (name) => { + const projectsService = getServiceHub().projects() + const newFolder = await projectsService.addProject(name) + const updatedProjects = await projectsService.getProjects() + set({ folders: updatedProjects }) + return newFolder + }, - updateFolder: (id, name) => { - set((state) => ({ - folders: state.folders.map((folder) => - folder.id === id - ? { ...folder, name, updated_at: Date.now() } - : folder - ), - })) - }, + updateFolder: async (id, name) => { + const projectsService = getServiceHub().projects() + await projectsService.updateProject(id, name) + const updatedProjects = await projectsService.getProjects() + set({ folders: updatedProjects }) + }, - deleteFolder: (id) => { - // Remove project metadata from all threads that belong to this project - const threadsState = useThreads.getState() - const threadsToUpdate = Object.values(threadsState.threads).filter( - (thread) => thread.metadata?.project?.id === id - ) + deleteFolder: async (id) => { + // Remove project metadata from all threads that belong to this project + const threadsState = useThreads.getState() + const threadsToUpdate = Object.values(threadsState.threads).filter( + (thread) => thread.metadata?.project?.id === id + ) - threadsToUpdate.forEach((thread) => { - threadsState.updateThread(thread.id, { - metadata: { - ...thread.metadata, - project: undefined, - }, - }) - }) + threadsToUpdate.forEach((thread) => { + threadsState.updateThread(thread.id, { + metadata: { + ...thread.metadata, + project: undefined, + }, + }) + }) - set((state) => ({ - folders: state.folders.filter((folder) => folder.id !== id), - })) - }, + const projectsService = getServiceHub().projects() + await projectsService.deleteProject(id) + const updatedProjects = await projectsService.getProjects() + set({ folders: updatedProjects }) + }, - getFolderById: (id) => { - return get().folders.find((folder) => folder.id === id) - }, - }), - { - name: localStorageKey.threadManagement, - storage: createJSONStorage(() => localStorage), + deleteFolderWithThreads: async (id) => { + // Get all threads that belong to this project + const threadsState = useThreads.getState() + const projectThreads = Object.values(threadsState.threads).filter( + (thread) => thread.metadata?.project?.id === id + ) + + // Delete threads from backend first + const serviceHub = getServiceHub() + for (const thread of projectThreads) { + await serviceHub.threads().deleteThread(thread.id) } - ) -) + + // Delete threads from frontend state + for (const thread of projectThreads) { + threadsState.deleteThread(thread.id) + } + + // Delete the project from storage + const projectsService = serviceHub.projects() + await projectsService.deleteProject(id) + + const updatedProjects = await projectsService.getProjects() + set({ folders: updatedProjects }) + }, + + getFolderById: (id) => { + return get().folders.find((folder) => folder.id === id) + }, + + getProjectById: async (id) => { + const projectsService = getServiceHub().projects() + return await projectsService.getProjectById(id) + }, +})) + +export const useThreadManagement = () => { + const store = useThreadManagementStore() + + // Load projects from service on mount + useEffect(() => { + const syncProjects = async () => { + try { + const projectsService = getServiceHub().projects() + const projects = await projectsService.getProjects() + useThreadManagementStore.setState({ folders: projects }) + } catch (error) { + console.error('Error syncing projects:', error) + } + } + syncProjects() + }, []) + + return store +} diff --git a/web-app/src/locales/en/common.json b/web-app/src/locales/en/common.json index c829dbdf8..bf246ba90 100644 --- a/web-app/src/locales/en/common.json +++ b/web-app/src/locales/en/common.json @@ -249,7 +249,11 @@ "projectNotFoundDesc": "The project you're looking for doesn't exist or has been deleted.", "deleteProjectDialog": { "title": "Delete Project", - "description": "Are you sure you want to delete this project? This action cannot be undone.", + "permanentDelete": "This will permanently delete all threads.", + "permanentDeleteWarning": "This action will permanently delete ALL threads within the project!", + "deleteEmptyProject": "This action will delete project \"{{projectName}}\".", + "saveThreadsAdvice": "To save threads, move them to your thread list or another project before deleting.", + "starredWarning": "You still have starred threads within the project.", "deleteButton": "Delete", "successWithName": "Project \"{{projectName}}\" deleted successfully", "successWithoutName": "Project deleted successfully", @@ -360,4 +364,5 @@ "description": "Thread removed from \"{{projectName}}\" successfully" } } -} \ No newline at end of file +} + diff --git a/web-app/src/routes/project/$projectId.tsx b/web-app/src/routes/project/$projectId.tsx index f25680112..042038e12 100644 --- a/web-app/src/routes/project/$projectId.tsx +++ b/web-app/src/routes/project/$projectId.tsx @@ -105,7 +105,7 @@ function ProjectPage() { {/* Thread List or Empty State */}
{projectThreads.length > 0 ? ( - + ) : (
state.threads) const [open, setOpen] = useState(false) @@ -48,19 +48,16 @@ function ProjectContent() { setDeleteConfirmOpen(true) } - const confirmDelete = () => { - if (deletingId) { - deleteFolder(deletingId) - setDeleteConfirmOpen(false) - setDeletingId(null) - } + const handleDeleteClose = () => { + setDeleteConfirmOpen(false) + setDeletingId(null) } - const handleSave = (name: string) => { + const handleSave = async (name: string) => { if (editingKey) { - updateFolder(editingKey, name) + await updateFolder(editingKey, name) } else { - const newProject = addFolder(name) + const newProject = await addFolder(name) // Navigate to the newly created project navigate({ to: '/project/$projectId', @@ -244,8 +241,8 @@ function ProjectContent() { />
diff --git a/web-app/src/services/index.ts b/web-app/src/services/index.ts index 121742177..0bfba90e6 100644 --- a/web-app/src/services/index.ts +++ b/web-app/src/services/index.ts @@ -26,6 +26,7 @@ import { DefaultUpdaterService } from './updater/default' import { DefaultPathService } from './path/default' import { DefaultCoreService } from './core/default' import { DefaultDeepLinkService } from './deeplink/default' +import { DefaultProjectsService } from './projects/default' // Import service types import type { ThemeService } from './theme/types' @@ -46,6 +47,7 @@ import type { UpdaterService } from './updater/types' import type { PathService } from './path/types' import type { CoreService } from './core/types' import type { DeepLinkService } from './deeplink/types' +import type { ProjectsService } from './projects/types' export interface ServiceHub { // Service getters - all synchronous after initialization @@ -67,6 +69,7 @@ export interface ServiceHub { path(): PathService core(): CoreService deeplink(): DeepLinkService + projects(): ProjectsService } class PlatformServiceHub implements ServiceHub { @@ -88,6 +91,7 @@ class PlatformServiceHub implements ServiceHub { private pathService: PathService = new DefaultPathService() private coreService: CoreService = new DefaultCoreService() private deepLinkService: DeepLinkService = new DefaultDeepLinkService() + private projectsService: ProjectsService = new DefaultProjectsService() private initialized = false /** @@ -158,6 +162,7 @@ class PlatformServiceHub implements ServiceHub { deepLinkModule, providersModule, mcpModule, + projectsModule, ] = await Promise.all([ import('./theme/web'), import('./app/web'), @@ -169,6 +174,7 @@ class PlatformServiceHub implements ServiceHub { import('./deeplink/web'), import('./providers/web'), import('./mcp/web'), + import('./projects/web'), ]) this.themeService = new themeModule.WebThemeService() @@ -181,6 +187,7 @@ class PlatformServiceHub implements ServiceHub { this.deepLinkService = new deepLinkModule.WebDeepLinkService() this.providersService = new providersModule.WebProvidersService() this.mcpService = new mcpModule.WebMCPService() + this.projectsService = new projectsModule.WebProjectsService() } this.initialized = true @@ -290,6 +297,11 @@ class PlatformServiceHub implements ServiceHub { this.ensureInitialized() return this.deepLinkService } + + projects(): ProjectsService { + this.ensureInitialized() + return this.projectsService + } } export async function initializeServiceHub(): Promise { diff --git a/web-app/src/services/projects/default.ts b/web-app/src/services/projects/default.ts new file mode 100644 index 000000000..e570453c9 --- /dev/null +++ b/web-app/src/services/projects/default.ts @@ -0,0 +1,78 @@ +/** + * Default Projects Service - localStorage implementation + */ + +import { ulid } from 'ulidx' +import type { ProjectsService, ThreadFolder } from './types' +import { localStorageKey } from '@/constants/localStorage' + +export class DefaultProjectsService implements ProjectsService { + private storageKey = localStorageKey.threadManagement + + private loadFromStorage(): ThreadFolder[] { + try { + const stored = localStorage.getItem(this.storageKey) + if (!stored) return [] + const data = JSON.parse(stored) + return data.state?.folders || [] + } catch (error) { + console.error('Error loading projects from localStorage:', error) + return [] + } + } + + private saveToStorage(projects: ThreadFolder[]): void { + try { + const data = { + state: { folders: projects }, + version: 0, + } + localStorage.setItem(this.storageKey, JSON.stringify(data)) + } catch (error) { + console.error('Error saving projects to localStorage:', error) + } + } + + async getProjects(): Promise { + return this.loadFromStorage() + } + + async addProject(name: string): Promise { + const newProject: ThreadFolder = { + id: ulid(), + name, + updated_at: Date.now(), + } + + const projects = this.loadFromStorage() + const updatedProjects = [...projects, newProject] + this.saveToStorage(updatedProjects) + + return newProject + } + + async updateProject(id: string, name: string): Promise { + const projects = this.loadFromStorage() + const updatedProjects = projects.map((project) => + project.id === id + ? { ...project, name, updated_at: Date.now() } + : project + ) + this.saveToStorage(updatedProjects) + } + + async deleteProject(id: string): Promise { + const projects = this.loadFromStorage() + const updatedProjects = projects.filter((project) => project.id !== id) + this.saveToStorage(updatedProjects) + } + + async getProjectById(id: string): Promise { + const projects = this.loadFromStorage() + return projects.find((project) => project.id === id) + } + + async setProjects(projects: ThreadFolder[]): Promise { + this.saveToStorage(projects) + } +} diff --git a/web-app/src/services/projects/types.ts b/web-app/src/services/projects/types.ts new file mode 100644 index 000000000..d4ddc83bd --- /dev/null +++ b/web-app/src/services/projects/types.ts @@ -0,0 +1,42 @@ +/** + * Projects Service Types + * Types for project/folder management operations + */ + +export interface ThreadFolder { + id: string + name: string + updated_at: number +} + +export interface ProjectsService { + /** + * Get all projects/folders + */ + getProjects(): Promise + + /** + * Add a new project/folder + */ + addProject(name: string): Promise + + /** + * Update a project/folder name + */ + updateProject(id: string, name: string): Promise + + /** + * Delete a project/folder + */ + deleteProject(id: string): Promise + + /** + * Get a project/folder by ID + */ + getProjectById(id: string): Promise + + /** + * Set all projects/folders (for bulk updates) + */ + setProjects(projects: ThreadFolder[]): Promise +} diff --git a/web-app/src/services/projects/web.ts b/web-app/src/services/projects/web.ts new file mode 100644 index 000000000..8dbaecd99 --- /dev/null +++ b/web-app/src/services/projects/web.ts @@ -0,0 +1,11 @@ +/** + * Web Projects Service - Web implementation + * Currently extends default, will be customized by extension-web team later + */ + +import { DefaultProjectsService } from './default' + +export class WebProjectsService extends DefaultProjectsService { + // Currently uses the same localStorage implementation as default + // Extension-web team can override methods here later +} diff --git a/web-app/src/services/threads/default.ts b/web-app/src/services/threads/default.ts index 72c66841a..bea5912a6 100644 --- a/web-app/src/services/threads/default.ts +++ b/web-app/src/services/threads/default.ts @@ -62,6 +62,7 @@ export class DefaultThreadsService implements ThreadsService { }, ], metadata: { + ...thread.metadata, order: thread.order, }, }) From 9f72debc1735b03af37bdf3432366cebca7a744c Mon Sep 17 00:00:00 2001 From: Dinh Long Nguyen Date: Wed, 1 Oct 2025 22:47:27 +0700 Subject: [PATCH 2/2] fix: thread item overfetching (#6699) * fix: thread item overfetching * chore: cleanup left over import --- web-app/src/providers/AuthProvider.tsx | 9 --------- web-app/src/providers/DataProvider.tsx | 10 +--------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/web-app/src/providers/AuthProvider.tsx b/web-app/src/providers/AuthProvider.tsx index a62ea4fdd..733296dd4 100644 --- a/web-app/src/providers/AuthProvider.tsx +++ b/web-app/src/providers/AuthProvider.tsx @@ -31,19 +31,10 @@ export function AuthProvider({ children }: AuthProviderProps) { const fetchUserData = useCallback(async () => { try { const { setThreads } = useThreads.getState() - const { setMessages } = useMessages.getState() // Fetch threads first const threads = await serviceHub.threads().fetchThreads() setThreads(threads) - - // Fetch messages for each thread - const messagePromises = threads.map(async (thread) => { - const messages = await serviceHub.messages().fetchMessages(thread.id) - setMessages(thread.id, messages) - }) - - await Promise.all(messagePromises) } catch (error) { console.error('Failed to fetch user data:', error) } diff --git a/web-app/src/providers/DataProvider.tsx b/web-app/src/providers/DataProvider.tsx index b8c928485..333bd2eec 100644 --- a/web-app/src/providers/DataProvider.tsx +++ b/web-app/src/providers/DataProvider.tsx @@ -1,4 +1,3 @@ -import { useMessages } from '@/hooks/useMessages' import { useModelProvider } from '@/hooks/useModelProvider' import { useAppUpdater } from '@/hooks/useAppUpdater' @@ -19,7 +18,6 @@ export function DataProvider() { const { setProviders, selectedModel, selectedProvider, getProviderByName } = useModelProvider() - const { setMessages } = useMessages() const { checkForUpdate } = useAppUpdater() const { setServers } = useMCPServers() const { setAssistants, initializeWithLastUsed } = useAssistant() @@ -87,14 +85,8 @@ export function DataProvider() { .fetchThreads() .then((threads) => { setThreads(threads) - threads.forEach((thread) => - serviceHub - .messages() - .fetchMessages(thread.id) - .then((messages) => setMessages(thread.id, messages)) - ) }) - }, [serviceHub, setThreads, setMessages]) + }, [serviceHub, setThreads]) // Check for app updates useEffect(() => {