Browse Source

feat: improved SVG output UX (#8765)

Hash Brown 6 months ago
parent
commit
3dfbc348e3

+ 13 - 0
web/app/components/base/chat/chat/answer/index.tsx

@@ -85,6 +85,19 @@ const Answer: FC<AnswerProps> = ({
       getContentWidth()
   }, [responding])
 
+  // Recalculate contentWidth when content changes (e.g., SVG preview/source toggle)
+  useEffect(() => {
+    if (!containerRef.current)
+      return
+    const resizeObserver = new ResizeObserver(() => {
+      getContentWidth()
+    })
+    resizeObserver.observe(containerRef.current)
+    return () => {
+      resizeObserver.disconnect()
+    }
+  }, [])
+
   return (
     <div className='flex mb-2 last:mb-0'>
       <div className='shrink-0 relative w-10 h-10'>

+ 70 - 49
web/app/components/base/markdown.tsx

@@ -116,59 +116,80 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
   const match = /language-(\w+)/.exec(className || '')
   const language = match?.[1]
   const languageShowName = getCorrectCapitalizationLanguageName(language || '')
-  let chartData = JSON.parse(String('{"title":{"text":"ECharts error - Wrong JSON format."}}').replace(/\n$/, ''))
-  if (language === 'echarts') {
-    try {
-      chartData = JSON.parse(String(children).replace(/\n$/, ''))
-    }
-    catch (error) {
+  const chartData = useMemo(() => {
+    if (language === 'echarts') {
+      try {
+        return JSON.parse(String(children).replace(/\n$/, ''))
+      }
+      catch (error) {}
     }
-  }
+    return JSON.parse('{"title":{"text":"ECharts error - Wrong JSON format."}}')
+  }, [language, children])
 
-  // Use `useMemo` to ensure that `SyntaxHighlighter` only re-renders when necessary
-  return useMemo(() => {
-    return (!inline && match)
-      ? (
-        <div>
-          <div
-            className='flex justify-between h-8 items-center p-1 pl-3 border-b'
-            style={{
-              borderColor: 'rgba(0, 0, 0, 0.05)',
-            }}
-          >
-            <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
-            <div style={{ display: 'flex' }}>
-              {language === 'mermaid' && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG}/>}
-              <CopyBtn
-                className='mr-1'
-                value={String(children).replace(/\n$/, '')}
-                isPlain
-              />
-            </div>
-          </div>
-          {(language === 'mermaid' && isSVG)
-            ? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />)
-            : (language === 'echarts'
-              ? (<div style={{ minHeight: '350px', minWidth: '700px' }}><ErrorBoundary><ReactEcharts option={chartData} /></ErrorBoundary></div>)
-              : (language === 'svg'
-                ? (<ErrorBoundary><SVGRenderer content={String(children).replace(/\n$/, '')} /></ErrorBoundary>)
-                : (<SyntaxHighlighter
-                  {...props}
-                  style={atelierHeathLight}
-                  customStyle={{
-                    paddingLeft: 12,
-                    backgroundColor: '#fff',
-                  }}
-                  language={match[1]}
-                  showLineNumbers
-                  PreTag="div"
-                >
-                  {String(children).replace(/\n$/, '')}
-                </SyntaxHighlighter>)))}
+  const renderCodeContent = useMemo(() => {
+    const content = String(children).replace(/\n$/, '')
+    if (language === 'mermaid' && isSVG) {
+      return <Flowchart PrimitiveCode={content} />
+    }
+    else if (language === 'echarts') {
+      return (
+        <div style={{ minHeight: '350px', minWidth: '700px' }}>
+          <ErrorBoundary>
+            <ReactEcharts option={chartData} />
+          </ErrorBoundary>
         </div>
       )
-      : (<code {...props} className={className}>{children}</code>)
-  }, [chartData, children, className, inline, isSVG, language, languageShowName, match, props])
+    }
+    else if (language === 'svg' && isSVG) {
+      return (
+        <ErrorBoundary>
+          <SVGRenderer content={content} />
+        </ErrorBoundary>
+      )
+    }
+    else {
+      return (
+        <SyntaxHighlighter
+          {...props}
+          style={atelierHeathLight}
+          customStyle={{
+            paddingLeft: 12,
+            backgroundColor: '#fff',
+          }}
+          language={match?.[1]}
+          showLineNumbers
+          PreTag="div"
+        >
+          {content}
+        </SyntaxHighlighter>
+      )
+    }
+  }, [language, match, props, children, chartData, isSVG])
+
+  if (inline || !match)
+    return <code {...props} className={className}>{children}</code>
+
+  return (
+    <div>
+      <div
+        className='flex justify-between h-8 items-center p-1 pl-3 border-b'
+        style={{
+          borderColor: 'rgba(0, 0, 0, 0.05)',
+        }}
+      >
+        <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
+        <div style={{ display: 'flex' }}>
+          {(['mermaid', 'svg']).includes(language!) && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG}/>}
+          <CopyBtn
+            className='mr-1'
+            value={String(children).replace(/\n$/, '')}
+            isPlain
+          />
+        </div>
+      </div>
+      {renderCodeContent}
+    </div>
+  )
 })
 CodeBlock.displayName = 'CodeBlock'
 

+ 8 - 10
web/app/components/base/svg-gallery/index.tsx

@@ -29,7 +29,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
     if (svgRef.current) {
       try {
         svgRef.current.innerHTML = ''
-        const draw = SVG().addTo(svgRef.current).size('100%', '100%')
+        const draw = SVG().addTo(svgRef.current)
 
         const parser = new DOMParser()
         const svgDoc = parser.parseFromString(content, 'image/svg+xml')
@@ -40,13 +40,11 @@ export const SVGRenderer = ({ content }: { content: string }) => {
 
         const originalWidth = parseInt(svgElement.getAttribute('width') || '400', 10)
         const originalHeight = parseInt(svgElement.getAttribute('height') || '600', 10)
-        const scale = Math.min(windowSize.width / originalWidth, windowSize.height / originalHeight, 1)
-        const scaledWidth = originalWidth * scale
-        const scaledHeight = originalHeight * scale
-        draw.size(scaledWidth, scaledHeight)
+        draw.viewbox(0, 0, originalWidth, originalHeight)
+
+        svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
 
         const rootElement = draw.svg(content)
-        rootElement.scale(scale)
 
         rootElement.click(() => {
           setImagePreview(svgToDataURL(svgElement as Element))
@@ -54,7 +52,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
       }
       catch (error) {
         if (svgRef.current)
-          svgRef.current.innerHTML = 'Error rendering SVG. Wait for the image content to complete.'
+          svgRef.current.innerHTML = '<span style="padding: 1rem;">Error rendering SVG. Wait for the image content to complete.</span>'
       }
     }
   }, [content, windowSize])
@@ -62,14 +60,14 @@ export const SVGRenderer = ({ content }: { content: string }) => {
   return (
     <>
       <div ref={svgRef} style={{
-        width: '100%',
-        height: '100%',
-        minHeight: '300px',
         maxHeight: '80vh',
         display: 'flex',
         justifyContent: 'center',
         alignItems: 'center',
         cursor: 'pointer',
+        wordBreak: 'break-word',
+        whiteSpace: 'normal',
+        margin: '0 auto',
       }} />
       {imagePreview && (<ImagePreview url={imagePreview} title='Preview' onCancel={() => setImagePreview('')} />)}
     </>

+ 4 - 2
web/app/components/workflow/panel/chat-record/index.tsx

@@ -90,7 +90,7 @@ const ChatRecord = () => {
   return (
     <div
       className={`
-        flex flex-col w-[400px] rounded-l-2xl h-full border border-black/2 shadow-xl
+        flex flex-col w-[420px] rounded-l-2xl h-full border border-black/2 shadow-xl
       `}
       style={{
         background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',
@@ -121,7 +121,7 @@ const ChatRecord = () => {
                 supportCitationHitInfo: true,
               } as any}
               chatList={chatList}
-              chatContainerClassName='px-4'
+              chatContainerClassName='px-3'
               chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto'
               chatFooterClassName='px-4 rounded-b-2xl'
               chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
@@ -129,6 +129,8 @@ const ChatRecord = () => {
               noChatInput
               allToolIcons={{}}
               showPromptLog
+              noSpacing
+              chatAnswerContainerInner='!pr-2'
             />
           </div>
         </>