mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(chat): render floating chat messages in insertion order instead of timestamp sort (#5647)
This commit is contained in:
@@ -401,9 +401,7 @@ export function Chat() {
|
||||
|
||||
const workflowMessages = useMemo(() => {
|
||||
if (!activeWorkflowId) return []
|
||||
return messages
|
||||
.filter((msg) => msg.workflowId === activeWorkflowId)
|
||||
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
||||
return messages.filter((msg) => msg.workflowId === activeWorkflowId)
|
||||
}, [messages, activeWorkflowId])
|
||||
|
||||
const isStreaming = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.hoisted(() => {
|
||||
const legacyNewestFirst = {
|
||||
state: {
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-2',
|
||||
content: 'response',
|
||||
workflowId: 'wf-1',
|
||||
type: 'workflow',
|
||||
timestamp: '2026-07-13T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-1',
|
||||
content: 'hi',
|
||||
workflowId: 'wf-1',
|
||||
type: 'user',
|
||||
timestamp: '2026-07-13T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
version: 0,
|
||||
}
|
||||
window.localStorage.setItem('chat-store', JSON.stringify(legacyNewestFirst))
|
||||
})
|
||||
|
||||
import { useChatStore } from '@/stores/chat/store'
|
||||
|
||||
describe('chat store message ordering', () => {
|
||||
it('migrates v0 persisted messages from newest-first to insertion order', () => {
|
||||
const messages = useChatStore.getState().messages
|
||||
expect(messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2'])
|
||||
})
|
||||
|
||||
describe('addMessage', () => {
|
||||
beforeEach(() => {
|
||||
useChatStore.setState({ messages: [] })
|
||||
})
|
||||
|
||||
it('appends messages so insertion order is conversation order, even with identical timestamps', () => {
|
||||
const { addMessage } = useChatStore.getState()
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
addMessage({ content: 'hi', workflowId: 'wf-1', type: 'user', timestamp } as any)
|
||||
addMessage({ content: '', workflowId: 'wf-1', type: 'workflow', timestamp } as any)
|
||||
|
||||
const types = useChatStore.getState().messages.map((m) => m.type)
|
||||
expect(types).toEqual(['user', 'workflow'])
|
||||
})
|
||||
|
||||
it('keeps only the most recent messages when trimming to the cap', () => {
|
||||
const { addMessage } = useChatStore.getState()
|
||||
|
||||
for (let i = 0; i < 55; i++) {
|
||||
addMessage({ content: `m${i}`, workflowId: 'wf-1', type: 'user' })
|
||||
}
|
||||
|
||||
const messages = useChatStore.getState().messages
|
||||
expect(messages).toHaveLength(50)
|
||||
expect(messages[0].content).toBe('m5')
|
||||
expect(messages[messages.length - 1].content).toBe('m54')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -63,7 +63,7 @@ export const useChatStore = create<ChatState>()(
|
||||
timestamp: (message as any).timestamp ?? new Date().toISOString(),
|
||||
}
|
||||
|
||||
const newMessages = [newMessage, ...state.messages].slice(0, MAX_MESSAGES)
|
||||
const newMessages = [...state.messages, newMessage].slice(-MAX_MESSAGES)
|
||||
|
||||
return { messages: newMessages }
|
||||
})
|
||||
@@ -126,14 +126,9 @@ export const useChatStore = create<ChatState>()(
|
||||
|
||||
const headers = ['timestamp', 'type', 'content']
|
||||
|
||||
const sortedMessages = [...messages].sort(
|
||||
(a: ChatMessage, b: ChatMessage) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
)
|
||||
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...sortedMessages.map((message: ChatMessage) =>
|
||||
...messages.map((message: ChatMessage) =>
|
||||
[
|
||||
formatCSVValue(message.timestamp),
|
||||
formatCSVValue(message.type),
|
||||
@@ -253,6 +248,21 @@ export const useChatStore = create<ChatState>()(
|
||||
}),
|
||||
{
|
||||
name: 'chat-store',
|
||||
version: 1,
|
||||
/**
|
||||
* v0 stored messages newest-first; v1 stores them in insertion
|
||||
* (chronological) order, which consumers render without sorting.
|
||||
*/
|
||||
migrate: (persistedState, version) => {
|
||||
if ((version ?? 0) < 1) {
|
||||
const state = persistedState as { messages?: ChatMessage[] } | null
|
||||
return {
|
||||
...state,
|
||||
messages: [...(state?.messages ?? [])].reverse(),
|
||||
}
|
||||
}
|
||||
return persistedState
|
||||
},
|
||||
/**
|
||||
* Persist only the durable chat state — message history (with transient
|
||||
* blob `previewUrl`s stripped since they are not valid across reloads),
|
||||
|
||||
Reference in New Issue
Block a user