Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions web-app/src/containers/ThreadList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -148,7 +148,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
onOpenChange={(open) => {
if (!open) {
setOpenDropdown(false)
setTitle(plainTitleForRename || 'New Thread');
setTitle(plainTitleForRename || 'New Thread')
}
}}
>
Expand Down Expand Up @@ -268,9 +268,14 @@ function ThreadList({ threads }: ThreadListProps) {

const sortedThreads = useMemo(() => {
return threads.sort((a, b) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sorting comparator logic is duplicated in both ThreadList and DataProvider. Consider extracting it into a shared utility to reduce maintenance overhead.

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])
Expand All @@ -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)
}
}
}}
Expand Down
68 changes: 40 additions & 28 deletions web-app/src/hooks/useThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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,
}),
}
})
},
Expand All @@ -119,7 +126,7 @@ export const useThreads = create<ThreadState>()(
})
return {
threads: {},
searchIndex: null // Or new Fzf([], {selector...})
searchIndex: null, // Or new Fzf([], {selector...})
}
})
},
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling get().setThreads inside a set() callback (in createThread) causes nested state updates. Consider merging updates into a single set call to simplify state management.


return {
threads: newThreads,
currentThreadId: createdThread.id,
searchIndex: new Fzf<Thread[]>(Object.values(newThreads), { selector: (item: Thread) => item.title }),
};
}
})
return createdThread
})
Expand Down Expand Up @@ -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,
}),
}
})
},
Expand Down
5 changes: 2 additions & 3 deletions web-app/src/providers/DataProvider.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions web-app/src/services/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const createThread = async (thread: Thread): Promise<Thread> => {
},
],
metadata: {
// order: 1,
order: thread.order,
},
})
.then((e) => {
Expand All @@ -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
})
Expand Down
Loading