mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(mothership): chat stability
This commit is contained in:
@@ -101,10 +101,10 @@ export function UserInput({
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
if (!isSending) handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
[handleSubmit, isSending]
|
||||
)
|
||||
|
||||
const toggleListening = useCallback(() => {
|
||||
|
||||
@@ -145,7 +145,7 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
<div className='flex h-full min-w-0 flex-1 flex-col'>
|
||||
<div className='min-h-0 flex-1 overflow-y-auto px-[24px] py-[16px]'>
|
||||
<div className='mx-auto max-w-[640px] space-y-[16px]'>
|
||||
{messages.map((msg) => {
|
||||
{messages.map((msg, index) => {
|
||||
if (msg.role === 'user') {
|
||||
return (
|
||||
<div key={msg.id} className='flex justify-end'>
|
||||
@@ -159,7 +159,8 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
}
|
||||
|
||||
const hasBlocks = msg.contentBlocks && msg.contentBlocks.length > 0
|
||||
const isThisStreaming = isSending && msg === messages[messages.length - 1]
|
||||
const isLastAssistant = msg.role === 'assistant' && index === messages.length - 1
|
||||
const isThisStreaming = isSending && isLastAssistant
|
||||
|
||||
if (!hasBlocks && !msg.content && isThisStreaming) {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
|
||||
import { tableKeys } from '@/hooks/queries/tables'
|
||||
import {
|
||||
@@ -116,8 +116,6 @@ function getPayloadData(payload: SSEPayload): SSEPayloadData | undefined {
|
||||
export function useChat(workspaceId: string, initialChatId?: string): UseChatReturn {
|
||||
const pathname = usePathname()
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const routerRef = useRef(router)
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -131,10 +129,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
const streamIdRef = useRef<string | undefined>(undefined)
|
||||
const sendingRef = useRef(false)
|
||||
const toolArgsMapRef = useRef<Map<string, Record<string, unknown>>>(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
routerRef.current = router
|
||||
}, [router])
|
||||
const streamGenRef = useRef(0)
|
||||
|
||||
const isHomePage = pathname.endsWith('/home')
|
||||
|
||||
@@ -157,6 +152,10 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (sendingRef.current) {
|
||||
chatIdRef.current = initialChatId
|
||||
return
|
||||
}
|
||||
chatIdRef.current = initialChatId
|
||||
appliedChatIdRef.current = undefined
|
||||
setMessages([])
|
||||
@@ -168,10 +167,12 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHomePage || !chatIdRef.current) return
|
||||
streamGenRef.current++
|
||||
chatIdRef.current = undefined
|
||||
appliedChatIdRef.current = undefined
|
||||
abortControllerRef.current?.abort()
|
||||
abortControllerRef.current = null
|
||||
sendingRef.current = false
|
||||
setMessages([])
|
||||
setError(null)
|
||||
setIsSending(false)
|
||||
@@ -252,7 +253,11 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
activeStreamId,
|
||||
})
|
||||
}
|
||||
routerRef.current.replace(`/workspace/${workspaceId}/task/${parsed.chatId}`)
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
`/workspace/${workspaceId}/task/${parsed.chatId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -407,6 +412,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
)
|
||||
|
||||
const finalize = useCallback(() => {
|
||||
sendingRef.current = false
|
||||
setIsSending(false)
|
||||
abortControllerRef.current = null
|
||||
chatBottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
@@ -422,8 +428,10 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
const activeStreamId = chatHistory?.activeStreamId
|
||||
if (!activeStreamId || !appliedChatIdRef.current || sendingRef.current) return
|
||||
|
||||
const gen = ++streamGenRef.current
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
sendingRef.current = true
|
||||
setIsSending(true)
|
||||
|
||||
const assistantId = crypto.randomUUID()
|
||||
@@ -442,7 +450,9 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') return
|
||||
} finally {
|
||||
finalize()
|
||||
if (streamGenRef.current === gen) {
|
||||
finalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
reconnect()
|
||||
@@ -468,6 +478,8 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
|
||||
abortControllerRef.current?.abort()
|
||||
|
||||
const gen = ++streamGenRef.current
|
||||
|
||||
setError(null)
|
||||
setIsSending(true)
|
||||
sendingRef.current = true
|
||||
@@ -530,16 +542,35 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
if (err instanceof Error && err.name === 'AbortError') return
|
||||
setError(err instanceof Error ? err.message : 'Failed to send message')
|
||||
} finally {
|
||||
sendingRef.current = false
|
||||
finalize()
|
||||
if (streamGenRef.current === gen) {
|
||||
finalize()
|
||||
}
|
||||
}
|
||||
},
|
||||
[workspaceId, queryClient, processSSEStream, finalize]
|
||||
)
|
||||
|
||||
const stopGeneration = useCallback(() => {
|
||||
streamGenRef.current++
|
||||
abortControllerRef.current?.abort()
|
||||
abortControllerRef.current = null
|
||||
sendingRef.current = false
|
||||
setIsSending(false)
|
||||
|
||||
const activeChatId = chatIdRef.current
|
||||
if (activeChatId) {
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.detail(activeChatId) })
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
|
||||
}, [workspaceId, queryClient])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
streamGenRef.current++
|
||||
abortControllerRef.current?.abort()
|
||||
abortControllerRef.current = null
|
||||
sendingRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
|
||||
@@ -166,6 +166,7 @@ export function Table({
|
||||
const [renameValue, setRenameValue] = useState('')
|
||||
const [deletingColumn, setDeletingColumn] = useState<string | null>(null)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isDraggingRef = useRef(false)
|
||||
|
||||
const { tableData, isLoadingTable, rows, isLoadingRows } = useTableData({
|
||||
@@ -362,12 +363,6 @@ export function Table({
|
||||
const column = columnsRef.current.find((c) => c.name === columnName)
|
||||
if (!column) return
|
||||
|
||||
if (column.type === 'json') {
|
||||
const row = rowsRef.current.find((r) => r.id === rowId)
|
||||
if (row) setEditingRow(row)
|
||||
return
|
||||
}
|
||||
|
||||
if (column.type === 'boolean') {
|
||||
const row = rowsRef.current.find((r) => r.id === rowId)
|
||||
if (row) {
|
||||
@@ -393,6 +388,8 @@ export function Table({
|
||||
const anchor = selectionAnchorRef.current
|
||||
if (!anchor || editingCellRef.current || editingEmptyCellRef.current) return
|
||||
|
||||
if (!containerRef.current?.contains(e.target as Node)) return
|
||||
|
||||
const cols = columnsRef.current
|
||||
const dataRows = visibleRowsRef.current
|
||||
const totalRows = dataRows.length + PLACEHOLDER_ROW_COUNT
|
||||
@@ -558,7 +555,7 @@ export function Table({
|
||||
if (selectionFocusRef.current !== null) return
|
||||
|
||||
const column = columnsRef.current.find((c) => c.name === columnName)
|
||||
if (!column || column.type === 'json' || column.type === 'boolean') return
|
||||
if (!column || column.type === 'boolean') return
|
||||
setEditingEmptyCell({ rowIndex, columnName })
|
||||
}, [])
|
||||
|
||||
@@ -738,7 +735,7 @@ export function Table({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div ref={containerRef} className='flex h-full flex-col'>
|
||||
{!embedded && (
|
||||
<>
|
||||
<ResourceHeader
|
||||
@@ -1290,27 +1287,15 @@ function CellContent({
|
||||
const isNull = value === null || value === undefined
|
||||
|
||||
if (column.type === 'boolean') {
|
||||
const boolValue = Boolean(value)
|
||||
return (
|
||||
<span className={boolValue ? 'text-green-500' : 'text-[var(--text-tertiary)]'}>
|
||||
{isNull ? '' : boolValue ? 'true' : 'false'}
|
||||
</span>
|
||||
)
|
||||
if (isNull) return null
|
||||
return <span className='text-[var(--text-primary)]'>{value ? 'true' : 'false'}</span>
|
||||
}
|
||||
|
||||
if (isNull) return null
|
||||
|
||||
if (column.type === 'json') {
|
||||
return (
|
||||
<span className='block truncate font-mono text-[11px] text-[var(--text-secondary)]'>
|
||||
{JSON.stringify(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (column.type === 'number') {
|
||||
return (
|
||||
<span className='font-mono text-[12px] text-[var(--text-secondary)]'>{String(value)}</span>
|
||||
<span className='block truncate text-[var(--text-primary)]'>{JSON.stringify(value)}</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1324,7 +1309,7 @@ function CellContent({
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
return <span className='text-[12px] text-[var(--text-secondary)]'>{formatted}</span>
|
||||
return <span className='text-[var(--text-primary)]'>{formatted}</span>
|
||||
} catch {
|
||||
return <span className='text-[var(--text-primary)]'>{String(value)}</span>
|
||||
}
|
||||
@@ -1399,12 +1384,7 @@ function InlineEditor({
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
className={cn(
|
||||
'w-full min-w-0 select-text border-none bg-transparent p-0 outline-none',
|
||||
column.type === 'number'
|
||||
? 'font-mono text-[12px] text-[var(--text-secondary)]'
|
||||
: column.type === 'date'
|
||||
? 'text-[12px] text-[var(--text-secondary)]'
|
||||
: 'text-[13px] text-[var(--text-primary)]'
|
||||
'w-full min-w-0 select-text border-none bg-transparent p-0 text-[13px] text-[var(--text-primary)] outline-none'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ export function cleanCellValue(value: unknown, column: ColumnDefinition): unknow
|
||||
export function formatValueForInput(value: unknown, type: string): string {
|
||||
if (value === null || value === undefined) return ''
|
||||
if (type === 'json') {
|
||||
return typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
||||
return typeof value === 'string' ? value : JSON.stringify(value)
|
||||
}
|
||||
if (type === 'date' && value) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user