mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Merge branch 'feat/mothership-copilot' of github.com:simstudioai/sim into feat/mothership-copilot
This commit is contained in:
@@ -167,12 +167,14 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
resources,
|
||||
isResourceCleanupSettled,
|
||||
activeResourceId,
|
||||
setActiveResourceId,
|
||||
} = useChat(workspaceId, chatId)
|
||||
|
||||
const [isResourceCollapsed, setIsResourceCollapsed] = useState(false)
|
||||
const [showExpandButton, setShowExpandButton] = useState(false)
|
||||
const [isResourceAnimatingIn, setIsResourceAnimatingIn] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResourceCollapsed) {
|
||||
@@ -186,16 +188,24 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
const collapseResource = useCallback(() => setIsResourceCollapsed(true), [])
|
||||
const expandResource = useCallback(() => setIsResourceCollapsed(false), [])
|
||||
|
||||
const prevResourceCountRef = useRef(resources.length)
|
||||
const animateResourcePanel =
|
||||
prevResourceCountRef.current === 0 && resources.length > 0 && isSending
|
||||
const visibleResources = isResourceCleanupSettled ? resources : []
|
||||
const prevResourceCountRef = useRef(visibleResources.length)
|
||||
const shouldEnterResourcePanel =
|
||||
isSending && prevResourceCountRef.current === 0 && visibleResources.length > 0
|
||||
useEffect(() => {
|
||||
if (animateResourcePanel) {
|
||||
if (shouldEnterResourcePanel) {
|
||||
const { isCollapsed, toggleCollapsed } = useSidebarStore.getState()
|
||||
if (!isCollapsed) toggleCollapsed()
|
||||
setIsResourceAnimatingIn(true)
|
||||
}
|
||||
prevResourceCountRef.current = resources.length
|
||||
})
|
||||
prevResourceCountRef.current = visibleResources.length
|
||||
}, [shouldEnterResourcePanel, visibleResources.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResourceAnimatingIn) return
|
||||
const timer = setTimeout(() => setIsResourceAnimatingIn(false), 400)
|
||||
return () => clearTimeout(timer)
|
||||
}, [isResourceAnimatingIn])
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(text: string, fileAttachments?: FileAttachmentForApi[]) => {
|
||||
@@ -340,19 +350,19 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resources.length > 0 && (
|
||||
{visibleResources.length > 0 && (
|
||||
<MothershipView
|
||||
workspaceId={workspaceId}
|
||||
resources={resources}
|
||||
resources={visibleResources}
|
||||
activeResourceId={activeResourceId}
|
||||
onSelectResource={setActiveResourceId}
|
||||
onCollapse={collapseResource}
|
||||
isCollapsed={isResourceCollapsed}
|
||||
className={animateResourcePanel ? 'animate-slide-in-right' : undefined}
|
||||
className={isResourceAnimatingIn ? 'animate-slide-in-right' : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{resources.length > 0 && showExpandButton && (
|
||||
{visibleResources.length > 0 && showExpandButton && (
|
||||
<div className='absolute top-[8.5px] right-[16px]'>
|
||||
<button
|
||||
type='button'
|
||||
|
||||
@@ -6,16 +6,15 @@ const BOTTOM_THRESHOLD = 30
|
||||
* Manages sticky auto-scroll for a streaming chat container.
|
||||
*
|
||||
* Stays pinned to the bottom while content streams in. Detaches when the user
|
||||
* explicitly scrolls up (wheel, touch, or scrollbar drag). Re-attaches when
|
||||
* the scroll position returns to within {@link BOTTOM_THRESHOLD} of the bottom.
|
||||
* scrolls beyond {@link BOTTOM_THRESHOLD} from the bottom. Re-attaches when
|
||||
* the scroll position returns within the threshold. Preserves bottom position
|
||||
* across container resizes (e.g. sidebar collapse).
|
||||
*/
|
||||
export function useAutoScroll(isStreaming: boolean) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const stickyRef = useRef(true)
|
||||
const prevScrollTopRef = useRef(0)
|
||||
const prevScrollHeightRef = useRef(0)
|
||||
const touchStartYRef = useRef(0)
|
||||
const atBottomRef = useRef(true)
|
||||
const rafIdRef = useRef(0)
|
||||
const teardownRef = useRef<(() => void) | null>(null)
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const el = containerRef.current
|
||||
@@ -24,8 +23,30 @@ export function useAutoScroll(isStreaming: boolean) {
|
||||
}, [])
|
||||
|
||||
const callbackRef = useCallback((el: HTMLDivElement | null) => {
|
||||
teardownRef.current?.()
|
||||
teardownRef.current = null
|
||||
containerRef.current = el
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
if (!el) return
|
||||
|
||||
el.scrollTop = el.scrollHeight
|
||||
atBottomRef.current = true
|
||||
|
||||
const onScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = el
|
||||
atBottomRef.current = scrollHeight - scrollTop - clientHeight <= BOTTOM_THRESHOLD
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (atBottomRef.current) el.scrollTop = el.scrollHeight
|
||||
})
|
||||
|
||||
el.addEventListener('scroll', onScroll, { passive: true })
|
||||
ro.observe(el)
|
||||
|
||||
teardownRef.current = () => {
|
||||
el.removeEventListener('scroll', onScroll)
|
||||
ro.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,71 +54,26 @@ export function useAutoScroll(isStreaming: boolean) {
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
|
||||
stickyRef.current = true
|
||||
prevScrollTopRef.current = el.scrollTop
|
||||
prevScrollHeightRef.current = el.scrollHeight
|
||||
atBottomRef.current = true
|
||||
scrollToBottom()
|
||||
|
||||
const detach = () => {
|
||||
stickyRef.current = false
|
||||
}
|
||||
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
if (e.deltaY < 0) detach()
|
||||
}
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
touchStartYRef.current = e.touches[0].clientY
|
||||
}
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (e.touches[0].clientY > touchStartYRef.current) detach()
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = el
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
||||
|
||||
if (distanceFromBottom <= BOTTOM_THRESHOLD) {
|
||||
stickyRef.current = true
|
||||
} else if (
|
||||
scrollTop < prevScrollTopRef.current &&
|
||||
scrollHeight <= prevScrollHeightRef.current
|
||||
) {
|
||||
stickyRef.current = false
|
||||
}
|
||||
|
||||
prevScrollTopRef.current = scrollTop
|
||||
prevScrollHeightRef.current = scrollHeight
|
||||
}
|
||||
|
||||
const guardedScroll = () => {
|
||||
if (stickyRef.current) scrollToBottom()
|
||||
if (atBottomRef.current) scrollToBottom()
|
||||
}
|
||||
|
||||
const onMutation = () => {
|
||||
prevScrollHeightRef.current = el.scrollHeight
|
||||
if (!stickyRef.current) return
|
||||
if (!atBottomRef.current) return
|
||||
cancelAnimationFrame(rafIdRef.current)
|
||||
rafIdRef.current = requestAnimationFrame(guardedScroll)
|
||||
}
|
||||
|
||||
el.addEventListener('wheel', onWheel, { passive: true })
|
||||
el.addEventListener('touchstart', onTouchStart, { passive: true })
|
||||
el.addEventListener('touchmove', onTouchMove, { passive: true })
|
||||
el.addEventListener('scroll', onScroll, { passive: true })
|
||||
|
||||
const observer = new MutationObserver(onMutation)
|
||||
observer.observe(el, { childList: true, subtree: true, characterData: true })
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('wheel', onWheel)
|
||||
el.removeEventListener('touchstart', onTouchStart)
|
||||
el.removeEventListener('touchmove', onTouchMove)
|
||||
el.removeEventListener('scroll', onScroll)
|
||||
observer.disconnect()
|
||||
cancelAnimationFrame(rafIdRef.current)
|
||||
if (stickyRef.current) scrollToBottom()
|
||||
if (atBottomRef.current) scrollToBottom()
|
||||
}
|
||||
}, [isStreaming, scrollToBottom])
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { usePathname } from 'next/navigation'
|
||||
@@ -6,7 +6,7 @@ import { executeRunToolOnClient } from '@/lib/copilot/client-sse/run-tool-execut
|
||||
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
|
||||
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
|
||||
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
|
||||
import { tableKeys } from '@/hooks/queries/tables'
|
||||
import { tableKeys, useTablesList } from '@/hooks/queries/tables'
|
||||
import {
|
||||
type TaskChatHistory,
|
||||
type TaskStoredContentBlock,
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
taskKeys,
|
||||
useChatHistory,
|
||||
} from '@/hooks/queries/tasks'
|
||||
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
|
||||
import { useWorkflows, workflowKeys } from '@/hooks/queries/workflows'
|
||||
import { useWorkspaceFiles, workspaceFilesKeys } from '@/hooks/queries/workspace-files'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import type { FileAttachmentForApi } from '../components/user-input/user-input'
|
||||
import type {
|
||||
@@ -48,6 +49,7 @@ export interface UseChatReturn {
|
||||
sendMessage: (message: string, fileAttachments?: FileAttachmentForApi[]) => Promise<void>
|
||||
stopGeneration: () => Promise<void>
|
||||
resources: MothershipResource[]
|
||||
isResourceCleanupSettled: boolean
|
||||
activeResourceId: string | null
|
||||
setActiveResourceId: (id: string | null) => void
|
||||
}
|
||||
@@ -57,6 +59,57 @@ const STATE_TO_STATUS: Record<string, ToolCallStatus> = {
|
||||
error: 'error',
|
||||
} as const
|
||||
|
||||
function areResourcesEqual(left: MothershipResource[], right: MothershipResource[]): boolean {
|
||||
if (left.length !== right.length) return false
|
||||
return left.every(
|
||||
(resource, index) =>
|
||||
resource.id === right[index]?.id &&
|
||||
resource.type === right[index]?.type &&
|
||||
resource.title === right[index]?.title
|
||||
)
|
||||
}
|
||||
|
||||
function sanitizeResources(
|
||||
resources: MothershipResource[],
|
||||
existingFileIds: Set<string>,
|
||||
existingTableIds: Set<string>,
|
||||
existingWorkflowIds: Set<string>,
|
||||
pendingFileIds: Set<string>,
|
||||
pendingTableIds: Set<string>,
|
||||
pendingWorkflowIds: Set<string>,
|
||||
shouldFilterMissingFiles: boolean,
|
||||
shouldFilterMissingTables: boolean,
|
||||
shouldFilterMissingWorkflows: boolean
|
||||
): MothershipResource[] {
|
||||
return resources.filter((resource) => {
|
||||
if (resource.type === 'file') {
|
||||
if (pendingFileIds.has(resource.id)) {
|
||||
return true
|
||||
}
|
||||
if (shouldFilterMissingFiles && !existingFileIds.has(resource.id)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (resource.type === 'table') {
|
||||
if (pendingTableIds.has(resource.id)) {
|
||||
return true
|
||||
}
|
||||
if (shouldFilterMissingTables && !existingTableIds.has(resource.id)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (resource.type === 'workflow') {
|
||||
if (pendingWorkflowIds.has(resource.id)) {
|
||||
return true
|
||||
}
|
||||
if (shouldFilterMissingWorkflows && !existingWorkflowIds.has(resource.id)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function mapStoredBlock(block: TaskStoredContentBlock): ContentBlock {
|
||||
const mapped: ContentBlock = {
|
||||
type: block.type as ContentBlockType,
|
||||
@@ -161,12 +214,55 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
const toolArgsMapRef = useRef<Map<string, Record<string, unknown>>>(new Map())
|
||||
const streamGenRef = useRef(0)
|
||||
const streamingContentRef = useRef('')
|
||||
const pendingFileResourceIdsRef = useRef<Set<string>>(new Set())
|
||||
const pendingTableResourceIdsRef = useRef<Set<string>>(new Set())
|
||||
const pendingWorkflowResourceIdsRef = useRef<Set<string>>(new Set())
|
||||
|
||||
const isHomePage = pathname.endsWith('/home')
|
||||
|
||||
const { data: chatHistory } = useChatHistory(initialChatId)
|
||||
const {
|
||||
data: workspaceFiles = [],
|
||||
isLoading: isWorkspaceFilesLoading,
|
||||
isError: isWorkspaceFilesError,
|
||||
} = useWorkspaceFiles(workspaceId)
|
||||
const {
|
||||
data: workspaceTables = [],
|
||||
isLoading: isWorkspaceTablesLoading,
|
||||
isError: isWorkspaceTablesError,
|
||||
} = useTablesList(workspaceId)
|
||||
const {
|
||||
data: workflows = [],
|
||||
isLoading: isWorkflowsLoading,
|
||||
isError: isWorkflowsError,
|
||||
} = useWorkflows(workspaceId, { syncRegistry: false })
|
||||
|
||||
const existingWorkspaceFileIds = useMemo(
|
||||
() => new Set(workspaceFiles.map((file) => file.id)),
|
||||
[workspaceFiles]
|
||||
)
|
||||
const existingWorkspaceTableIds = useMemo(
|
||||
() => new Set(workspaceTables.map((table) => table.id)),
|
||||
[workspaceTables]
|
||||
)
|
||||
const existingWorkflowIds = useMemo(
|
||||
() => new Set(workflows.map((workflow) => workflow.id)),
|
||||
[workflows]
|
||||
)
|
||||
const isResourceCleanupSettled = useMemo(
|
||||
() => !isWorkspaceFilesLoading && !isWorkspaceTablesLoading && !isWorkflowsLoading,
|
||||
[isWorkspaceFilesLoading, isWorkspaceTablesLoading, isWorkflowsLoading]
|
||||
)
|
||||
|
||||
const addResource = useCallback((resource: MothershipResource) => {
|
||||
if (resource.type === 'file') {
|
||||
pendingFileResourceIdsRef.current.add(resource.id)
|
||||
} else if (resource.type === 'table') {
|
||||
pendingTableResourceIdsRef.current.add(resource.id)
|
||||
} else if (resource.type === 'workflow') {
|
||||
pendingWorkflowResourceIdsRef.current.add(resource.id)
|
||||
}
|
||||
|
||||
setResources((prev) => {
|
||||
const existing = prev.find((r) => r.type === resource.type && r.id === resource.id)
|
||||
if (existing) {
|
||||
@@ -182,6 +278,30 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
setActiveResourceId(resource.id)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
for (const id of pendingFileResourceIdsRef.current) {
|
||||
if (existingWorkspaceFileIds.has(id)) {
|
||||
pendingFileResourceIdsRef.current.delete(id)
|
||||
}
|
||||
}
|
||||
}, [existingWorkspaceFileIds])
|
||||
|
||||
useEffect(() => {
|
||||
for (const id of pendingTableResourceIdsRef.current) {
|
||||
if (existingWorkspaceTableIds.has(id)) {
|
||||
pendingTableResourceIdsRef.current.delete(id)
|
||||
}
|
||||
}
|
||||
}, [existingWorkspaceTableIds])
|
||||
|
||||
useEffect(() => {
|
||||
for (const id of pendingWorkflowResourceIdsRef.current) {
|
||||
if (existingWorkflowIds.has(id)) {
|
||||
pendingWorkflowResourceIdsRef.current.delete(id)
|
||||
}
|
||||
}
|
||||
}, [existingWorkflowIds])
|
||||
|
||||
useEffect(() => {
|
||||
if (sendingRef.current) {
|
||||
chatIdRef.current = initialChatId
|
||||
@@ -194,6 +314,9 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
setIsSending(false)
|
||||
setResources([])
|
||||
setActiveResourceId(null)
|
||||
pendingFileResourceIdsRef.current.clear()
|
||||
pendingTableResourceIdsRef.current.clear()
|
||||
pendingWorkflowResourceIdsRef.current.clear()
|
||||
}, [initialChatId])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -209,6 +332,9 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
setIsSending(false)
|
||||
setResources([])
|
||||
setActiveResourceId(null)
|
||||
pendingFileResourceIdsRef.current.clear()
|
||||
pendingTableResourceIdsRef.current.clear()
|
||||
pendingWorkflowResourceIdsRef.current.clear()
|
||||
}, [isHomePage])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -245,6 +371,51 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
}
|
||||
}, [chatHistory, workspaceId])
|
||||
|
||||
useEffect(() => {
|
||||
setResources((prev) => {
|
||||
const shouldFilterMissingFiles = !isWorkspaceFilesLoading && !isWorkspaceFilesError
|
||||
const shouldFilterMissingTables = !isWorkspaceTablesLoading && !isWorkspaceTablesError
|
||||
const shouldFilterMissingWorkflows = !isWorkflowsLoading && !isWorkflowsError
|
||||
const next = sanitizeResources(
|
||||
prev,
|
||||
existingWorkspaceFileIds,
|
||||
existingWorkspaceTableIds,
|
||||
existingWorkflowIds,
|
||||
pendingFileResourceIdsRef.current,
|
||||
pendingTableResourceIdsRef.current,
|
||||
pendingWorkflowResourceIdsRef.current,
|
||||
shouldFilterMissingFiles,
|
||||
shouldFilterMissingTables,
|
||||
shouldFilterMissingWorkflows
|
||||
)
|
||||
return areResourcesEqual(prev, next) ? prev : next
|
||||
})
|
||||
}, [
|
||||
resources,
|
||||
existingWorkspaceFileIds,
|
||||
existingWorkspaceTableIds,
|
||||
existingWorkflowIds,
|
||||
isWorkspaceFilesError,
|
||||
isWorkspaceFilesLoading,
|
||||
isWorkspaceTablesError,
|
||||
isWorkspaceTablesLoading,
|
||||
isWorkflowsError,
|
||||
isWorkflowsLoading,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (resources.length === 0) {
|
||||
if (activeResourceId !== null) {
|
||||
setActiveResourceId(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!activeResourceId || !resources.some((resource) => resource.id === activeResourceId)) {
|
||||
setActiveResourceId(resources[resources.length - 1].id)
|
||||
}
|
||||
}, [activeResourceId, resources])
|
||||
|
||||
const processSSEStream = useCallback(
|
||||
async (reader: ReadableStreamDefaultReader<Uint8Array>, assistantId: string) => {
|
||||
const decoder = new TextDecoder()
|
||||
@@ -256,6 +427,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
let lastTableId: string | null = null
|
||||
let lastWorkflowId: string | null = null
|
||||
let runningText = ''
|
||||
let lastContentSource: 'main' | 'subagent' | null = null
|
||||
|
||||
streamingContentRef.current = ''
|
||||
toolArgsMapRef.current.clear()
|
||||
@@ -326,9 +498,17 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
case 'content': {
|
||||
const chunk = typeof parsed.data === 'string' ? parsed.data : (parsed.content ?? '')
|
||||
if (chunk) {
|
||||
const contentSource: 'main' | 'subagent' = activeSubagent ? 'subagent' : 'main'
|
||||
const needsBoundaryNewline =
|
||||
lastContentSource !== null &&
|
||||
lastContentSource !== contentSource &&
|
||||
runningText.length > 0 &&
|
||||
!runningText.endsWith('\n')
|
||||
const tb = ensureTextBlock()
|
||||
tb.content = (tb.content ?? '') + chunk
|
||||
runningText += chunk
|
||||
const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk
|
||||
tb.content = (tb.content ?? '') + normalizedChunk
|
||||
runningText += normalizedChunk
|
||||
lastContentSource = contentSource
|
||||
streamingContentRef.current = runningText
|
||||
flush()
|
||||
}
|
||||
@@ -425,6 +605,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
resource = extractTableResource(parsed, storedArgs, lastTableId)
|
||||
if (resource) {
|
||||
lastTableId = resource.id
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.list(workspaceId) })
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.detail(resource.id) })
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(resource.id) })
|
||||
}
|
||||
@@ -444,6 +625,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
if (resource) {
|
||||
if (resource.type === 'table') {
|
||||
lastTableId = resource.id
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.list(workspaceId) })
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.detail(resource.id) })
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(resource.id) })
|
||||
} else if (resource.type === 'file') {
|
||||
@@ -459,6 +641,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
resource = extractFunctionExecuteResource(parsed, storedArgs)
|
||||
if (resource?.type === 'table') {
|
||||
lastTableId = resource.id
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.list(workspaceId) })
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.detail(resource.id) })
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(resource.id) })
|
||||
}
|
||||
@@ -466,6 +649,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
resource = extractWorkflowResource(parsed, lastWorkflowId, storedArgs)
|
||||
if (resource) {
|
||||
lastWorkflowId = resource.id
|
||||
queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) })
|
||||
const registry = useWorkflowRegistry.getState()
|
||||
if (!registry.workflows[resource.id]) {
|
||||
useWorkflowRegistry.setState((state) => ({
|
||||
@@ -761,6 +945,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
sendMessage,
|
||||
stopGeneration,
|
||||
resources,
|
||||
isResourceCleanupSettled,
|
||||
activeResourceId,
|
||||
setActiveResourceId,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user