fix: thread list state order after dragable (#5141)

* fix: thread list state order after dragable

* fix: new chat order

* chore: revert data provider
This commit is contained in:
Faisal Amir 2025-05-30 00:00:26 +07:00 committed by GitHub
parent 27c2a360f0
commit 426dc2ab87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 70 additions and 46 deletions

View File

@ -83,10 +83,10 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
const plainTitleForRename = useMemo(() => {
// Basic HTML stripping for simple span tags.
// If thread.title is undefined or null, treat as empty string before replace.
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '');
}, [thread.title]);
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
}, [thread.title])
const [title, setTitle] = useState(plainTitleForRename || 'New Thread');
const [title, setTitle] = useState(plainTitleForRename || 'New Thread')
return (
<div
@ -148,7 +148,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
onOpenChange={(open) => {
if (!open) {
setOpenDropdown(false)
setTitle(plainTitleForRename || 'New Thread');
setTitle(plainTitleForRename || 'New Thread')
}
}}
>
@ -268,9 +268,14 @@ function ThreadList({ threads }: ThreadListProps) {
const sortedThreads = useMemo(() => {
return threads.sort((a, b) => {
if (a.order && b.order) return a.order - b.order
// Later on top
// If both have order, sort by order (ascending, so lower order comes first)
if (a.order != null && b.order != null) {
return a.order - b.order
}
// If only one has order, prioritize the one with order (order comes first)
if (a.order != null) return -1
if (b.order != null) return 1
// If neither has order, sort by updated time (newer threads first)
return (b.updated || 0) - (a.updated || 0)
})
}, [threads])
@ -293,17 +298,25 @@ function ThreadList({ threads }: ThreadListProps) {
const { active, over } = event
if (active.id !== over?.id && over) {
// Access Global State
const allThreadsMap = useThreads.getState().threads;
const allThreadsArray = Object.values(allThreadsMap);
const allThreadsMap = useThreads.getState().threads
const allThreadsArray = Object.values(allThreadsMap)
// Calculate Global Indices
const oldIndexInGlobal = allThreadsArray.findIndex((t) => t.id === active.id);
const newIndexInGlobal = allThreadsArray.findIndex((t) => t.id === over.id);
const oldIndexInGlobal = allThreadsArray.findIndex(
(t) => t.id === active.id
)
const newIndexInGlobal = allThreadsArray.findIndex(
(t) => t.id === over.id
)
// Reorder Globally and Update State
if (oldIndexInGlobal !== -1 && newIndexInGlobal !== -1) {
const reorderedGlobalThreads = arrayMove(allThreadsArray, oldIndexInGlobal, newIndexInGlobal);
setThreads(reorderedGlobalThreads);
const reorderedGlobalThreads = arrayMove(
allThreadsArray,
oldIndexInGlobal,
newIndexInGlobal
)
setThreads(reorderedGlobalThreads)
}
}
}}

View File

@ -46,10 +46,14 @@ export const useThreads = create<ThreadState>()(
(acc: Record<string, Thread>, thread) => {
acc[thread.id] = thread
return acc
}, {} as Record<string, Thread>)
},
{} as Record<string, Thread>
)
set({
threads: threadMap,
searchIndex: new Fzf<Thread[]>(Object.values(threadMap), { selector: (item: Thread) => item.title })
searchIndex: new Fzf<Thread[]>(Object.values(threadMap), {
selector: (item: Thread) => item.title,
}),
})
},
getFilteredThreads: (searchTerm: string) => {
@ -63,25 +67,26 @@ export const useThreads = create<ThreadState>()(
let currentIndex = searchIndex
if (!currentIndex) {
currentIndex = new Fzf<Thread[]>(
Object.values(threads),
{ selector: (item: Thread) => item.title }
)
currentIndex = new Fzf<Thread[]>(Object.values(threads), {
selector: (item: Thread) => item.title,
})
set({ searchIndex: currentIndex })
}
// Use the index to search and return matching threads
const fzfResults = currentIndex.find(searchTerm)
return fzfResults.map((result: { item: Thread; positions: Set<number> }) => {
const thread = result.item; // Fzf stores the original item here
// Ensure result.positions is an array, default to empty if undefined
const positions = Array.from(result.positions) || [];
const highlightedTitle = highlightFzfMatch(thread.title, positions);
return {
...thread,
title: highlightedTitle, // Override title with highlighted version
};
});
return fzfResults.map(
(result: { item: Thread; positions: Set<number> }) => {
const thread = result.item // Fzf stores the original item here
// Ensure result.positions is an array, default to empty if undefined
const positions = Array.from(result.positions) || []
const highlightedTitle = highlightFzfMatch(thread.title, positions)
return {
...thread,
title: highlightedTitle, // Override title with highlighted version
}
}
)
},
toggleFavorite: (threadId) => {
set((state) => {
@ -107,7 +112,9 @@ export const useThreads = create<ThreadState>()(
deleteThread(threadId)
return {
threads: remainingThreads,
searchIndex: new Fzf<Thread[]>(Object.values(remainingThreads), { selector: (item: Thread) => item.title })
searchIndex: new Fzf<Thread[]>(Object.values(remainingThreads), {
selector: (item: Thread) => item.title,
}),
}
})
},
@ -119,7 +126,7 @@ export const useThreads = create<ThreadState>()(
})
return {
threads: {},
searchIndex: null // Or new Fzf([], {selector...})
searchIndex: null, // Or new Fzf([], {selector...})
}
})
},
@ -157,21 +164,24 @@ export const useThreads = create<ThreadState>()(
id: ulid(),
title: title ?? 'New Thread',
model,
// order: 1,
order: 1, // Will be set properly by setThreads
updated: Date.now() / 1000,
assistants: assistant ? [assistant] : [],
}
return await createThread(newThread).then((createdThread) => {
set((state) => {
const newThreads = {
...state.threads,
[createdThread.id]: createdThread,
};
// Get all existing threads as an array
const existingThreads = Object.values(state.threads)
// Create new array with the new thread at the beginning
const reorderedThreads = [createdThread, ...existingThreads]
// Use setThreads to handle proper ordering (this will assign order 1, 2, 3...)
get().setThreads(reorderedThreads)
return {
threads: newThreads,
currentThreadId: createdThread.id,
searchIndex: new Fzf<Thread[]>(Object.values(newThreads), { selector: (item: Thread) => item.title }),
};
}
})
return createdThread
})
@ -221,10 +231,12 @@ export const useThreads = create<ThreadState>()(
title: newTitle,
}
updateThread(updatedThread) // External call, order is fine
const newThreads = { ...state.threads, [threadId]: updatedThread };
const newThreads = { ...state.threads, [threadId]: updatedThread }
return {
threads: newThreads,
searchIndex: new Fzf<Thread[]>(Object.values(newThreads), { selector: (item: Thread) => item.title }),
searchIndex: new Fzf<Thread[]>(Object.values(newThreads), {
selector: (item: Thread) => item.title,
}),
}
})
},

View File

@ -1,6 +1,6 @@
import { useMessages } from '@/hooks/useMessages'
import { useModelProvider } from '@/hooks/useModelProvider'
import { useThreads } from '@/hooks/useThreads'
import { useAppUpdater } from '@/hooks/useAppUpdater'
import { fetchMessages } from '@/services/messages'
import { fetchModels } from '@/services/models'
@ -15,7 +15,7 @@ import { getAssistants } from '@/services/assistants'
export function DataProvider() {
const { setProviders } = useModelProvider()
const { setThreads } = useThreads()
const { setMessages } = useMessages()
const { checkForUpdate } = useAppUpdater()
const { setServers } = useMCPServers()
@ -35,7 +35,6 @@ export function DataProvider() {
useEffect(() => {
fetchThreads().then((threads) => {
setThreads(threads)
threads.forEach((thread) =>
fetchMessages(thread.id).then((messages) =>
setMessages(thread.id, messages)

View File

@ -56,7 +56,7 @@ export const createThread = async (thread: Thread): Promise<Thread> => {
},
],
metadata: {
// order: 1,
order: thread.order,
},
})
.then((e) => {
@ -67,7 +67,7 @@ export const createThread = async (thread: Thread): Promise<Thread> => {
id: e.assistants?.[0]?.model?.id,
provider: e.assistants?.[0]?.model?.engine,
},
// order: 1,
order: e.metadata?.order ?? thread.order,
assistants: e.assistants ?? [defaultAssistant],
} as Thread
})