mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
Fixes
This commit is contained in:
@@ -1025,6 +1025,7 @@ const DocxPreview = memo(function DocxPreview({
|
||||
file: WorkspaceFileRecord
|
||||
workspaceId: string
|
||||
}) {
|
||||
const viewportRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
data: fileData,
|
||||
@@ -1032,6 +1033,40 @@ const DocxPreview = memo(function DocxPreview({
|
||||
error: fetchError,
|
||||
} = useWorkspaceFileBinary(workspaceId, file.id, file.key)
|
||||
const [renderError, setRenderError] = useState<string | null>(null)
|
||||
const [docxScale, setDocxScale] = useState(1)
|
||||
const [scaledSize, setScaledSize] = useState<{ width: number; height: number } | null>(null)
|
||||
|
||||
const updateDocxScale = useCallback(() => {
|
||||
const viewport = viewportRef.current
|
||||
const container = containerRef.current
|
||||
if (!viewport || !container) return
|
||||
|
||||
const intrinsicWidth = container.scrollWidth
|
||||
const intrinsicHeight = container.scrollHeight
|
||||
if (intrinsicWidth === 0 || intrinsicHeight === 0) return
|
||||
|
||||
const viewportStyle = window.getComputedStyle(viewport)
|
||||
const paddingX =
|
||||
Number.parseFloat(viewportStyle.paddingLeft) + Number.parseFloat(viewportStyle.paddingRight)
|
||||
const availableWidth = Math.max(viewport.clientWidth - paddingX, 0)
|
||||
const nextScale = availableWidth > 0 ? Math.min(1, availableWidth / intrinsicWidth) : 1
|
||||
|
||||
setDocxScale((prev) => (Math.abs(prev - nextScale) < 0.001 ? prev : nextScale))
|
||||
setScaledSize((prev) => {
|
||||
const next = {
|
||||
width: intrinsicWidth * nextScale,
|
||||
height: intrinsicHeight * nextScale,
|
||||
}
|
||||
if (
|
||||
prev &&
|
||||
Math.abs(prev.width - next.width) < 1 &&
|
||||
Math.abs(prev.height - next.height) < 1
|
||||
) {
|
||||
return prev
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !fileData) return
|
||||
@@ -1042,12 +1077,18 @@ const DocxPreview = memo(function DocxPreview({
|
||||
try {
|
||||
const { renderAsync } = await import('docx-preview')
|
||||
if (cancelled || !containerRef.current) return
|
||||
setRenderError(null)
|
||||
setDocxScale(1)
|
||||
setScaledSize(null)
|
||||
containerRef.current.innerHTML = ''
|
||||
await renderAsync(fileData, containerRef.current, undefined, {
|
||||
inWrapper: true,
|
||||
ignoreWidth: false,
|
||||
ignoreHeight: false,
|
||||
})
|
||||
if (!cancelled) {
|
||||
requestAnimationFrame(updateDocxScale)
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed to render document'
|
||||
@@ -1061,13 +1102,57 @@ const DocxPreview = memo(function DocxPreview({
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [fileData])
|
||||
}, [fileData, updateDocxScale])
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = viewportRef.current
|
||||
const container = containerRef.current
|
||||
if (!viewport || !container) return
|
||||
|
||||
updateDocxScale()
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateDocxScale()
|
||||
})
|
||||
|
||||
resizeObserver.observe(viewport)
|
||||
resizeObserver.observe(container)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [fileData, updateDocxScale])
|
||||
|
||||
const error = resolvePreviewError(fetchError, renderError)
|
||||
if (error) return <PreviewError label='document' error={error} />
|
||||
if (isLoading) return DOCUMENT_SKELETON
|
||||
|
||||
return <div ref={containerRef} className='h-full w-full overflow-auto bg-white' />
|
||||
return (
|
||||
<div ref={viewportRef} className='h-full overflow-auto bg-[var(--surface-1)] p-4 sm:p-6'>
|
||||
<div className='flex min-h-full justify-center'>
|
||||
<div
|
||||
className='shrink-0'
|
||||
style={
|
||||
scaledSize
|
||||
? {
|
||||
width: scaledSize.width,
|
||||
minHeight: scaledSize.height,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='origin-top'
|
||||
style={{
|
||||
transform: `scale(${docxScale})`,
|
||||
transformOrigin: 'top center',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const pptxSlideCache = new Map<string, string[]>()
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRe
|
||||
import { useRouter } from 'next/navigation'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Streamdown } from 'streamdown'
|
||||
import 'streamdown/styles.css'
|
||||
import { Checkbox } from '@/components/emcn'
|
||||
@@ -74,7 +75,7 @@ export const PreviewPanel = memo(function PreviewPanel({
|
||||
return null
|
||||
})
|
||||
|
||||
const REMARK_PLUGINS = [remarkBreaks]
|
||||
const REMARK_PLUGINS = [remarkGfm, remarkBreaks]
|
||||
const REHYPE_PLUGINS = [rehypeSlug]
|
||||
|
||||
/**
|
||||
@@ -404,12 +405,80 @@ const MarkdownPreview = memo(function MarkdownPreview({
|
||||
)
|
||||
})
|
||||
|
||||
const HTML_PREVIEW_BASE_URL = 'about:srcdoc'
|
||||
|
||||
const HTML_PREVIEW_CSP = [
|
||||
"default-src 'none'",
|
||||
"script-src 'unsafe-inline'",
|
||||
"style-src 'unsafe-inline'",
|
||||
'img-src data: blob:',
|
||||
'font-src data:',
|
||||
'media-src data: blob:',
|
||||
"connect-src 'none'",
|
||||
"form-action 'none'",
|
||||
"frame-src 'none'",
|
||||
"child-src 'none'",
|
||||
"object-src 'none'",
|
||||
].join('; ')
|
||||
|
||||
const HTML_PREVIEW_BOOTSTRAP = `<script>
|
||||
(() => {
|
||||
const allowHref = (href) => href.startsWith('#') || /^\\s*javascript:/i.test(href)
|
||||
|
||||
document.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
if (!(event.target instanceof Element)) return
|
||||
const anchor = event.target.closest('a[href]')
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return
|
||||
const href = anchor.getAttribute('href') || ''
|
||||
if (allowHref(href)) return
|
||||
event.preventDefault()
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
document.addEventListener(
|
||||
'submit',
|
||||
(event) => {
|
||||
event.preventDefault()
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
})()
|
||||
</script>`
|
||||
|
||||
function buildHtmlPreviewDocument(content: string): string {
|
||||
const headInjection = [
|
||||
'<meta charset="utf-8">',
|
||||
`<base href="${HTML_PREVIEW_BASE_URL}">`,
|
||||
`<meta http-equiv="Content-Security-Policy" content="${HTML_PREVIEW_CSP}">`,
|
||||
HTML_PREVIEW_BOOTSTRAP,
|
||||
].join('')
|
||||
|
||||
if (/<head[\s>]/i.test(content)) {
|
||||
return content.replace(/<head(\s[^>]*)?>/i, (match) => `${match}${headInjection}`)
|
||||
}
|
||||
|
||||
if (/<html[\s>]/i.test(content)) {
|
||||
return content.replace(/<html(\s[^>]*)?>/i, (match) => `${match}<head>${headInjection}</head>`)
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html><html><head>${headInjection}</head><body>${content}</body></html>`
|
||||
}
|
||||
|
||||
const HtmlPreview = memo(function HtmlPreview({ content }: { content: string }) {
|
||||
// Run inline HTML/JS in an isolated iframe while blocking any navigation
|
||||
// that would replace the preview with another document.
|
||||
const wrappedContent = useMemo(() => buildHtmlPreviewDocument(content), [content])
|
||||
|
||||
return (
|
||||
<div className='h-full overflow-hidden'>
|
||||
<iframe
|
||||
srcDoc={content}
|
||||
sandbox='allow-same-origin'
|
||||
srcDoc={wrappedContent}
|
||||
sandbox='allow-scripts'
|
||||
referrerPolicy='no-referrer'
|
||||
title='HTML Preview'
|
||||
className='h-full w-full border-0 bg-white'
|
||||
/>
|
||||
|
||||
@@ -2507,6 +2507,13 @@ export function useChat(
|
||||
return
|
||||
}
|
||||
|
||||
// A live SSE `complete` event is already terminal. Finalize immediately so follow-up
|
||||
// sends do not get spuriously queued behind an already-finished response.
|
||||
if (streamResult.sawComplete) {
|
||||
finalize()
|
||||
return
|
||||
}
|
||||
|
||||
await resumeOrFinalize({
|
||||
streamId: streamIdRef.current || userMessageId,
|
||||
assistantId,
|
||||
|
||||
Reference in New Issue
Block a user