Skip to content

Commit 0074fab

Browse files
committed
refactor use chat
1 parent c7d0b97 commit 0074fab

30 files changed

Lines changed: 2930 additions & 2140 deletions

.github/CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
# Client-side stream consumption, hydration, and reconnect.
2525
/apps/sim/app/workspace/*/home/hooks/index.ts @simstudioai/mothership
2626
/apps/sim/app/workspace/*/home/hooks/use-chat.ts @simstudioai/mothership
27-
/apps/sim/app/workspace/*/home/hooks/use-file-preview-sessions.ts @simstudioai/mothership
27+
/apps/sim/app/workspace/*/home/hooks/preview/ @simstudioai/mothership
28+
/apps/sim/app/workspace/*/home/hooks/stream/ @simstudioai/mothership
2829
/apps/sim/hooks/queries/tasks.ts @simstudioai/mothership

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/docx-preview.tsx

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { createLogger } from '@sim/logger'
55
import { toError } from '@sim/utils/errors'
66
import { cn } from '@/lib/core/utils/cn'
77
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
8-
import { useWorkspaceFileBinary } from '@/hooks/queries/workspace-files'
98
import { PDF_PAGE_SKELETON, PreviewError, resolvePreviewError } from './preview-shared'
109
import { PreviewToolbar } from './preview-toolbar'
1110
import { bindPreviewWheelZoom } from './preview-wheel-zoom'
11+
import { useDocPreviewBinary } from './use-doc-preview-binary'
1212

1313
const logger = createLogger('DocxPreview')
1414

@@ -62,30 +62,15 @@ function fitDocxToContainer(host: HTMLElement, viewport: HTMLElement, zoomPercen
6262
export const DocxPreview = memo(function DocxPreview({
6363
file,
6464
workspaceId,
65-
streamingContent,
6665
}: {
6766
file: WorkspaceFileRecord
6867
workspaceId: string
69-
streamingContent?: string
7068
}) {
7169
const containerRef = useRef<HTMLDivElement>(null)
7270
const scrollContainerRef = useRef<HTMLDivElement>(null)
7371
const zoomPercentRef = useRef(100)
74-
// Generated docs are 0 bytes until the tool commits the compiled source at the
75-
// end of the run; only fetch the compiled artifact once content exists so we
76-
// don't 409-poll the serve route throughout generation. Uploaded docs always
77-
// have size > 0, so they fetch immediately as before.
78-
const hasCommittedContent = (file.size ?? 0) > 0
79-
const {
80-
data: fileData,
81-
isLoading,
82-
error: fetchError,
83-
} = useWorkspaceFileBinary(workspaceId, file.id, file.key, {
84-
enabled: hasCommittedContent,
85-
// edit_content updates in place (same storage key); version on updatedAt so an
86-
// open preview refetches the new binary instead of showing the stale one.
87-
version: Number(new Date(file.updatedAt)) || file.size,
88-
})
72+
const preview = useDocPreviewBinary(workspaceId, file)
73+
const fileData = preview.data
8974
const [renderError, setRenderError] = useState<string | null>(null)
9075
const [rendering, setRendering] = useState(false)
9176
const [hasRenderedPreview, setHasRenderedPreview] = useState(false)
@@ -191,7 +176,7 @@ export const DocxPreview = memo(function DocxPreview({
191176
}, [pageCount, documentRenderVersion])
192177

193178
useEffect(() => {
194-
if (!containerRef.current || !fileData || streamingContent !== undefined) return
179+
if (!containerRef.current || !fileData) return
195180

196181
let cancelled = false
197182

@@ -229,18 +214,12 @@ export const DocxPreview = memo(function DocxPreview({
229214
return () => {
230215
cancelled = true
231216
}
232-
}, [fileData, streamingContent, applyPostRenderStyling])
217+
}, [fileData, applyPostRenderStyling])
233218

234-
// The document is compiled to a committed binary (E2B doc sandbox, or
235-
// isolated-vm when disabled) and rendered from the committed artifact above.
236-
// There is no live per-tick preview: while the agent is still generating
237-
// (streamingContent defined), the skeleton shows until the artifact lands.
238-
const error = streamingContent !== undefined ? null : resolvePreviewError(fetchError, renderError)
219+
const error = resolvePreviewError(preview.error, renderError)
239220
if (error) return <PreviewError label='document' error={error} />
240221

241-
const showSkeleton =
242-
!hasRenderedPreview &&
243-
(streamingContent !== undefined || isLoading || rendering || !hasCommittedContent)
222+
const showSkeleton = !hasRenderedPreview && (!fileData || rendering)
244223

245224
const scrollToPage = (page: number) => {
246225
const scrollContainer = scrollContainerRef.current

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 15 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getFileExtension } from '@/lib/uploads/utils/file-utils'
99
import { useWorkspaceFileBinary } from '@/hooks/queries/workspace-files'
1010
import { resolveFileCategory } from './file-category'
1111
import type { StreamingMode } from './text-editor-state'
12+
import { useDocPreviewBinary } from './use-doc-preview-binary'
1213

1314
export type { StreamingMode } from './text-editor-state'
1415

@@ -88,14 +89,7 @@ export function FileViewer({
8889
}
8990

9091
if (category === 'iframe-previewable') {
91-
return (
92-
<IframePreview
93-
key={file.id}
94-
file={file}
95-
workspaceId={workspaceId}
96-
streamingContent={streamingContent}
97-
/>
98-
)
92+
return <IframePreview key={file.id} file={file} workspaceId={workspaceId} />
9993
}
10094

10195
if (category === 'image-previewable') {
@@ -111,29 +105,15 @@ export function FileViewer({
111105
}
112106

113107
if (category === 'docx-previewable') {
114-
return (
115-
<DocxPreview
116-
key={file.id}
117-
file={file}
118-
workspaceId={workspaceId}
119-
streamingContent={streamingContent}
120-
/>
121-
)
108+
return <DocxPreview key={file.id} file={file} workspaceId={workspaceId} />
122109
}
123110

124111
if (category === 'pptx-previewable') {
125-
return (
126-
<PptxPreview
127-
key={file.id}
128-
file={file}
129-
workspaceId={workspaceId}
130-
streamingContent={streamingContent}
131-
/>
132-
)
112+
return <PptxPreview key={file.id} file={file} workspaceId={workspaceId} />
133113
}
134114

135115
if (category === 'xlsx-previewable') {
136-
return <XlsxPreview file={file} workspaceId={workspaceId} />
116+
return <XlsxPreview key={file.id} file={file} workspaceId={workspaceId} />
137117
}
138118

139119
return <UnsupportedPreview file={file} />
@@ -142,45 +122,30 @@ export function FileViewer({
142122
const IframePreview = memo(function IframePreview({
143123
file,
144124
workspaceId,
145-
streamingContent,
146125
}: {
147126
file: WorkspaceFileRecord
148127
workspaceId: string
149-
streamingContent?: string
150128
}) {
151-
// Generated PDFs are 0 bytes until the tool commits the compiled source at the
152-
// end of the run; only fetch the compiled artifact once content exists so we
153-
// don't 409-poll the serve route throughout generation. Uploaded PDFs always
154-
// have size > 0, so they fetch immediately as before. Re-fetches when the file
155-
// record is invalidated, so the preview renders once the artifact lands.
156-
const {
157-
data: fileData,
158-
isLoading,
159-
error: fetchError,
160-
dataUpdatedAt,
161-
} = useWorkspaceFileBinary(workspaceId, file.id, file.key, {
162-
enabled: (file.size ?? 0) > 0,
163-
// edit_content updates in place (same storage key); version on updatedAt so an
164-
// open preview refetches the new binary instead of showing the stale one.
165-
version: Number(new Date(file.updatedAt)) || file.size,
166-
})
129+
const preview = useDocPreviewBinary(workspaceId, file)
167130

168131
const bufferSource = useMemo<PdfDocumentSource | null>(
169-
() => (fileData ? { kind: 'buffer', buffer: fileData } : null),
170-
[fileData]
132+
() => (preview.data ? { kind: 'buffer', buffer: preview.data } : null),
133+
[preview.data]
171134
)
172135

173-
// No live per-tick preview: suppress transient fetch errors while generating
174-
// (streamingContent defined) so the skeleton shows instead of a flash.
175-
const error = streamingContent !== undefined ? null : resolvePreviewError(fetchError, null)
136+
const error = resolvePreviewError(preview.error, null)
176137
if (error) return <PreviewError label='PDF' error={error} />
177138

178-
if (streamingContent !== undefined || isLoading || !bufferSource) {
139+
if (!bufferSource) {
179140
return <div className='relative flex flex-1 overflow-hidden'>{PDF_PAGE_SKELETON}</div>
180141
}
181142

182143
return (
183-
<PdfViewerCore key={`${file.id}:${dataUpdatedAt}`} source={bufferSource} filename={file.name} />
144+
<PdfViewerCore
145+
key={`${file.id}:${preview.dataUpdatedAt}`}
146+
source={bufferSource}
147+
filename={file.name}
148+
/>
184149
)
185150
})
186151

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { resolveFileCategory } from './file-category'
12
export type { PreviewMode } from './file-viewer'
23
export { FileViewer, isPreviewable, isTextEditable } from './file-viewer'
34
export { RICH_PREVIEWABLE_EXTENSIONS } from './preview-panel'

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pptx-preview.tsx

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
PreviewError,
1010
resolvePreviewError,
1111
} from '@/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared'
12-
import { useWorkspaceFileBinary } from '@/hooks/queries/workspace-files'
12+
import { useDocPreviewBinary } from '@/app/workspace/[workspaceId]/files/components/file-viewer/use-doc-preview-binary'
1313

1414
const logger = createLogger('PptxPreview')
1515

@@ -44,36 +44,16 @@ function pptxCacheKey(fileId: string, dataUpdatedAt: number, byteLength: number)
4444
export const PptxPreview = memo(function PptxPreview({
4545
file,
4646
workspaceId,
47-
streamingContent,
4847
}: {
4948
file: WorkspaceFileRecord
5049
workspaceId: string
51-
streamingContent?: string
5250
}) {
53-
// Generated decks are 0 bytes until the tool commits the compiled source at the
54-
// end of the run; only fetch the compiled artifact once content exists so we
55-
// don't 409-poll the serve route throughout generation. Uploaded decks always
56-
// have size > 0, so they fetch immediately as before.
57-
const {
58-
data: fileData,
59-
error: fetchError,
60-
dataUpdatedAt,
61-
} = useWorkspaceFileBinary(workspaceId, file.id, file.key, {
62-
enabled: (file.size ?? 0) > 0,
63-
// edit_content updates in place (same storage key); version on updatedAt so an
64-
// open preview refetches the new binary instead of showing the stale one.
65-
version: Number(new Date(file.updatedAt)) || file.size,
66-
})
67-
68-
const cacheKey = pptxCacheKey(file.id, dataUpdatedAt, fileData?.byteLength ?? 0)
51+
const preview = useDocPreviewBinary(workspaceId, file)
52+
const fileData = preview.data
53+
const cacheKey = pptxCacheKey(file.id, preview.dataUpdatedAt, fileData?.byteLength ?? 0)
6954

7055
const [hasRendered, setHasRendered] = useState(false)
7156
const [renderError, setRenderError] = useState<string | null>(null)
72-
// The deck is compiled to a committed binary (E2B doc sandbox, or isolated-vm
73-
// when disabled) and served by useWorkspaceFileBinary. There is no live per-tick
74-
// preview: while the agent is still generating (isStreaming), we show the
75-
// loading skeleton and render the committed artifact once it lands/updates.
76-
const isStreaming = streamingContent !== undefined
7757

7858
useEffect(() => {
7959
setRenderError(null)
@@ -93,9 +73,7 @@ export const PptxPreview = memo(function PptxPreview({
9373
setRenderError(message || 'Failed to render presentation')
9474
}
9575

96-
// Suppress transient fetch errors while generating — show the skeleton instead
97-
// of a "failed to preview" flash until the committed artifact is ready.
98-
const error = isStreaming ? null : resolvePreviewError(fetchError, renderError)
76+
const error = resolvePreviewError(preview.error, renderError)
9977

10078
if (error) return <PreviewError label='presentation' error={error} />
10179

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { resolveDocPreviewBinary } from './use-doc-preview-binary'
6+
7+
function buffer(byte: number): ArrayBuffer {
8+
return new Uint8Array([byte]).buffer
9+
}
10+
11+
describe('resolveDocPreviewBinary', () => {
12+
it('reports empty when nothing is committed and no binary has resolved', () => {
13+
const result = resolveDocPreviewBinary({
14+
data: undefined,
15+
isPlaceholderData: false,
16+
error: null,
17+
lastGood: null,
18+
hasCommittedContent: false,
19+
})
20+
21+
expect(result.state).toBe('empty')
22+
expect(result.data).toBeNull()
23+
expect(result.error).toBeNull()
24+
})
25+
26+
it('reports loading when committed but the first fetch has not resolved', () => {
27+
const result = resolveDocPreviewBinary({
28+
data: undefined,
29+
isPlaceholderData: false,
30+
error: null,
31+
lastGood: null,
32+
hasCommittedContent: true,
33+
})
34+
35+
expect(result.state).toBe('loading')
36+
expect(result.data).toBeNull()
37+
})
38+
39+
it('reports ready and advances the head on a fresh success', () => {
40+
const fresh = buffer(1)
41+
const result = resolveDocPreviewBinary({
42+
data: fresh,
43+
isPlaceholderData: false,
44+
error: null,
45+
lastGood: null,
46+
hasCommittedContent: true,
47+
})
48+
49+
expect(result.state).toBe('ready')
50+
expect(result.data).toBe(fresh)
51+
expect(result.lastGood).toBe(fresh)
52+
})
53+
54+
it('holds the previous binary as stale while a new version is fetching', () => {
55+
const previous = buffer(1)
56+
const result = resolveDocPreviewBinary({
57+
data: previous,
58+
isPlaceholderData: true,
59+
error: null,
60+
lastGood: previous,
61+
hasCommittedContent: true,
62+
})
63+
64+
expect(result.state).toBe('stale')
65+
expect(result.data).toBe(previous)
66+
})
67+
68+
it('falls back to the last good binary and suppresses the error after a failed refetch', () => {
69+
const previous = buffer(1)
70+
const result = resolveDocPreviewBinary({
71+
data: undefined,
72+
isPlaceholderData: false,
73+
error: new Error('boom'),
74+
lastGood: previous,
75+
hasCommittedContent: true,
76+
})
77+
78+
expect(result.state).toBe('stale')
79+
expect(result.data).toBe(previous)
80+
expect(result.error).toBeNull()
81+
})
82+
83+
it('surfaces the error only when there is no binary to fall back to', () => {
84+
const err = new Error('missing artifact')
85+
const result = resolveDocPreviewBinary({
86+
data: undefined,
87+
isPlaceholderData: false,
88+
error: err,
89+
lastGood: null,
90+
hasCommittedContent: true,
91+
})
92+
93+
expect(result.data).toBeNull()
94+
expect(result.error).toBe(err)
95+
})
96+
})

0 commit comments

Comments
 (0)