diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bae6ee3e..f7858c4c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,7 +36,7 @@ "react-force-graph-2d": "^1.29.1", "react-google-drive-picker": "^1.2.2", "react-i18next": "^17.0.13", - "react-markdown": "^9.1.0", + "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "react-router-dom": "^7.18.3", "react-syntax-highlighter": "^16.1.1", @@ -10425,9 +10425,9 @@ } }, "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", diff --git a/frontend/package.json b/frontend/package.json index 00a997d0..7e8d2857 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,7 +52,7 @@ "react-force-graph-2d": "^1.29.1", "react-google-drive-picker": "^1.2.2", "react-i18next": "^17.0.13", - "react-markdown": "^9.1.0", + "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "react-router-dom": "^7.18.3", "react-syntax-highlighter": "^16.1.1", diff --git a/frontend/src/components/ArtifactSidebar.tsx b/frontend/src/components/ArtifactSidebar.tsx index 5788bb01..fe3ddaaf 100644 --- a/frontend/src/components/ArtifactSidebar.tsx +++ b/frontend/src/components/ArtifactSidebar.tsx @@ -165,105 +165,106 @@ function NoteView({ data }: { data: NoteArtifactData }) {
{data.content ? ( - + -
- - {language} - - + return match ? ( +
+
+ + {language} + + +
+ + {String(children).replace(/\n$/, '')} +
- - {String(children).replace(/\n$/, '')} - -
- ) : ( - - {children} - - ); - }, - ul({ children }) { - return ( -
    - {children} -
- ); - }, - ol({ children }) { - return ( -
    - {children} -
- ); - }, - a({ children, href }) { - return ( - - {children} - - ); - }, - p({ children }) { - return

{children}

; - }, - h1({ children }) { - return

{children}

; - }, - h2({ children }) { - return

{children}

; - }, - h3({ children }) { - return

{children}

; - }, - blockquote({ children }) { - return ( -
- {children} -
- ); - }, - }} - > - {data.content} -
+ {children} + + ); + }, + ul({ children }) { + return ( + + ); + }, + ol({ children }) { + return ( +
    + {children} +
+ ); + }, + a({ children, href }) { + return ( + + {children} + + ); + }, + p({ children }) { + return

{children}

; + }, + h1({ children }) { + return

{children}

; + }, + h2({ children }) { + return

{children}

; + }, + h3({ children }) { + return

{children}

; + }, + blockquote({ children }) { + return ( +
+ {children} +
+ ); + }, + }} + > + {data.content} +
+
) : (

Empty note

)} diff --git a/frontend/src/components/MarkdownPreview.tsx b/frontend/src/components/MarkdownPreview.tsx index 4a2634e5..d671e8e1 100644 --- a/frontend/src/components/MarkdownPreview.tsx +++ b/frontend/src/components/MarkdownPreview.tsx @@ -9,66 +9,67 @@ import remarkGfm from 'remark-gfm'; export default function MarkdownPreview({ content }: { content: string }) { return (
- - {children} - - ); - }, - ul({ children }) { - return ( - - ); - }, - ol({ children }) { - return ( -
    - {children} -
- ); - }, - h1({ children }) { - return

{children}

; - }, - h2({ children }) { - return

{children}

; - }, - h3({ children }) { - return

{children}

; - }, - code({ children, ...rest }) { - return ( - - {children} - - ); - }, - blockquote({ children }) { - return ( -
- {children} -
- ); - }, - }} - > - {content} -
+
+ + {children} + + ); + }, + ul({ children }) { + return ( +
    + {children} +
+ ); + }, + ol({ children }) { + return ( +
    + {children} +
+ ); + }, + h1({ children }) { + return

{children}

; + }, + h2({ children }) { + return

{children}

; + }, + h3({ children }) { + return

{children}

; + }, + code({ children, ...rest }) { + return ( + + {children} + + ); + }, + blockquote({ children }) { + return ( +
+ {children} +
+ ); + }, + }} + > + {content} +
+
); } diff --git a/frontend/src/conversation/MarkdownAnswer.tsx b/frontend/src/conversation/MarkdownAnswer.tsx index b85e58fb..9d41dcd0 100644 --- a/frontend/src/conversation/MarkdownAnswer.tsx +++ b/frontend/src/conversation/MarkdownAnswer.tsx @@ -184,177 +184,180 @@ export default function MarkdownAnswer({ {contentSegments.map((segment, index) => ( {segment.type === 'text' ? ( - {children}; - } - if (sandboxLink.kind === 'artifact') { - return renderArtifactChip(sandboxLink.artifact, children); - } - if (href?.startsWith('#cite-')) { - const num = href.replace('#cite-', ''); - const sourceIdx = parseInt(num, 10) - 1; - return ( - - ); - } - return ( - - {children} - - ); - }, - img({ src, alt }) { - // `![chart](sandbox:/mnt/data/chart.png)` is how a model - // announces a plot it produced. react-markdown runs - // urlTransform over `src` too, so without this the invented - // scheme survives and renders a broken-image box beside the - // chip that opens the very same file. - const source = typeof src === 'string' ? src : undefined; - const sandboxLink = resolveSandboxLink( - source, - artifacts, - turnArtifacts, - ); - if (sandboxLink.kind === 'artifact') { - const { artifact } = sandboxLink; - return renderArtifactChip( - artifact, - alt || artifact.label || 'Open file', - ); - } - if (sandboxLink.kind === 'plain') { - return <>{alt ?? ''}; - } - return {alt}; - }, - code(props) { - const { children, className, node, ref, ...rest } = props; - const match = /language-(\w+)/.exec(className || ''); - const language = match ? match[1] : ''; - - return match ? ( -
-
- - {language} - - -
- - {String(children).replace(/\n$/, '')} - -
- ) : ( - - {children} - - ); - }, - ul({ children }) { - return ( - - ); - }, - ol({ children }) { - return ( -
    - {children} -
- ); - }, - table({ children }) { - return ( -
- + if (el) { + el.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + el.classList.add('ring-2', 'ring-purple-500'); + setTimeout( + () => + el.classList.remove( + 'ring-2', + 'ring-purple-500', + ), + 2000, + ); + } + }} + className="mx-0.5 h-5 min-w-5 rounded-full bg-purple-100 px-1.5 text-xs font-semibold text-purple-700 hover:bg-purple-200 dark:bg-purple-900/40 dark:text-purple-300 dark:hover:bg-purple-900/60" + title={`Jump to source ${num}`} + > + {num} + + ); + } + return ( + {children} -
-
- ); - }, - thead({ children }) { - return ( - - {children} - - ); - }, - tr({ children }) { - return ( - - {children} - - ); - }, - th({ children }) { - return {children}; - }, - td({ children }) { - return {children}; - }, - }} - > - {segment.content} -
+ + ); + }, + img({ src, alt }) { + // `![chart](sandbox:/mnt/data/chart.png)` is how a model + // announces a plot it produced. react-markdown runs + // urlTransform over `src` too, so without this the invented + // scheme survives and renders a broken-image box beside the + // chip that opens the very same file. + const source = typeof src === 'string' ? src : undefined; + const sandboxLink = resolveSandboxLink( + source, + artifacts, + turnArtifacts, + ); + if (sandboxLink.kind === 'artifact') { + const { artifact } = sandboxLink; + return renderArtifactChip( + artifact, + alt || artifact.label || 'Open file', + ); + } + if (sandboxLink.kind === 'plain') { + return <>{alt ?? ''}; + } + return ( + {alt} + ); + }, + code(props) { + const { children, className, node, ref, ...rest } = props; + const match = /language-(\w+)/.exec(className || ''); + const language = match ? match[1] : ''; + + return match ? ( +
+
+ + {language} + + +
+ + {String(children).replace(/\n$/, '')} + +
+ ) : ( + + {children} + + ); + }, + ul({ children }) { + return ( + + ); + }, + ol({ children }) { + return ( +
    + {children} +
+ ); + }, + table({ children }) { + return ( +
+ + {children} +
+
+ ); + }, + thead({ children }) { + return ( + + {children} + + ); + }, + tr({ children }) { + return ( + + {children} + + ); + }, + th({ children }) { + return {children}; + }, + td({ children }) { + return {children}; + }, + }} + > + {segment.content} + + ) : (