Render streamed assistant responses as Markdown
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@douyinfe/semi-icons": "2.103.0",
|
"@douyinfe/semi-icons": "2.103.0",
|
||||||
"@douyinfe/semi-ui-19": "2.103.0",
|
"@douyinfe/semi-ui-19": "2.103.0",
|
||||||
"ai": "6.0.277",
|
"ai": "6.0.277",
|
||||||
|
"markstream-react": "2.0.8",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+122
@@ -20,6 +20,9 @@ importers:
|
|||||||
ai:
|
ai:
|
||||||
specifier: 6.0.277
|
specifier: 6.0.277
|
||||||
version: 6.0.277(zod@4.5.4)
|
version: 6.0.277(zod@4.5.4)
|
||||||
|
markstream-react:
|
||||||
|
specifier: 2.0.8
|
||||||
|
version: 2.0.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
react:
|
react:
|
||||||
specifier: 19.2.8
|
specifier: 19.2.8
|
||||||
version: 19.2.8
|
version: 19.2.8
|
||||||
@@ -728,6 +731,10 @@ packages:
|
|||||||
electron-to-chromium@1.5.422:
|
electron-to-chromium@1.5.422:
|
||||||
resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==}
|
resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==}
|
||||||
|
|
||||||
|
entities@8.1.0:
|
||||||
|
resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
esast-util-from-estree@2.0.0:
|
esast-util-from-estree@2.0.0:
|
||||||
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
|
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
|
||||||
|
|
||||||
@@ -916,6 +923,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
linkify-it@6.1.0:
|
||||||
|
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||||
|
|
||||||
linkifyjs@4.3.3:
|
linkifyjs@4.3.3:
|
||||||
resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==}
|
resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==}
|
||||||
|
|
||||||
@@ -939,9 +949,56 @@ packages:
|
|||||||
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
|
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
markdown-it-container@4.0.0:
|
||||||
|
resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==}
|
||||||
|
|
||||||
|
markdown-it-footnote@4.0.0:
|
||||||
|
resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==}
|
||||||
|
|
||||||
|
markdown-it-ins@4.0.0:
|
||||||
|
resolution: {integrity: sha512-sWbjK2DprrkINE4oYDhHdCijGT+MIDhEupjSHLXe5UXeVr5qmVxs/nTUVtgi0Oh/qtF+QKV0tNWDhQBEPxiMew==}
|
||||||
|
|
||||||
|
markdown-it-mark@4.0.0:
|
||||||
|
resolution: {integrity: sha512-YLhzaOsU9THO/cal0lUjfMjrqSMPjjyjChYM7oyj4DnyaXEzA8gnW6cVJeyCrCVeyesrY2PlEdUYJSPFYL4Nkg==}
|
||||||
|
|
||||||
|
markdown-it-sup@2.0.0:
|
||||||
|
resolution: {integrity: sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA==}
|
||||||
|
|
||||||
|
markdown-it-task-checkbox@1.0.6:
|
||||||
|
resolution: {integrity: sha512-7pxkHuvqTOu3iwVGmDPeYjQg+AIS9VQxzyLP9JCg9lBjgPAJXGEkChK6A2iFuj3tS0GV3HG2u5AMNhcQqwxpJw==}
|
||||||
|
|
||||||
|
markdown-it-ts@1.1.2:
|
||||||
|
resolution: {integrity: sha512-17qO3LQLK+JXom5Zt6Z9l7OGWWv9eYquTxdj3xIPGe1OBHdYXrzNYA3xCgCyIj1uItv4jkqbQf6FkaqifKkySw==}
|
||||||
|
engines: {node: '>=20.19'}
|
||||||
|
|
||||||
markdown-table@3.0.4:
|
markdown-table@3.0.4:
|
||||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||||
|
|
||||||
|
markstream-core@2.0.8:
|
||||||
|
resolution: {integrity: sha512-3VLe2fMDhoe0SnH5OXsmdXfT6vrsY8/WetNE02xR8AuTaP7ixH2tBl9dHTsLgiLMp/z8D+TB0xVP/tkLzkmNlA==}
|
||||||
|
|
||||||
|
markstream-react@2.0.8:
|
||||||
|
resolution: {integrity: sha512-LRWCOkzvl4T4d1IWBAG3buJBnazNX1qmkND9ns66RGdF3un194CTXS9A/Qc0kZT7NvIcJvCsCiLoDJ42J1UAOw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@antv/infographic': ^0.2.3
|
||||||
|
'@terrastruct/d2': '>=0.1.33'
|
||||||
|
katex: '>=0.16.22'
|
||||||
|
mermaid: '>=11'
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
stream-diffs: '>=0.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@antv/infographic':
|
||||||
|
optional: true
|
||||||
|
'@terrastruct/d2':
|
||||||
|
optional: true
|
||||||
|
katex:
|
||||||
|
optional: true
|
||||||
|
mermaid:
|
||||||
|
optional: true
|
||||||
|
stream-diffs:
|
||||||
|
optional: true
|
||||||
|
|
||||||
mdast-util-find-and-replace@3.0.2:
|
mdast-util-find-and-replace@3.0.2:
|
||||||
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
|
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
|
||||||
|
|
||||||
@@ -990,6 +1047,9 @@ packages:
|
|||||||
mdast-util-to-string@4.0.0:
|
mdast-util-to-string@4.0.0:
|
||||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||||
|
|
||||||
|
mdurl@2.1.0:
|
||||||
|
resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==}
|
||||||
|
|
||||||
memoize-one@5.2.1:
|
memoize-one@5.2.1:
|
||||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||||
|
|
||||||
@@ -1195,6 +1255,10 @@ packages:
|
|||||||
prosemirror-view@1.42.3:
|
prosemirror-view@1.42.3:
|
||||||
resolution: {integrity: sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==}
|
resolution: {integrity: sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==}
|
||||||
|
|
||||||
|
punycode.js@2.3.1:
|
||||||
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
react-dom@19.2.8:
|
react-dom@19.2.8:
|
||||||
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
|
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1291,6 +1355,9 @@ packages:
|
|||||||
space-separated-tokens@2.0.2:
|
space-separated-tokens@2.0.2:
|
||||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||||
|
|
||||||
|
stream-markdown-parser@1.2.14:
|
||||||
|
resolution: {integrity: sha512-7nZR7ZUM8uPmXGMZPcP3wPtb40Ce8NUJy6XDGmengRENxE824VD6o6dbPvosUVE07mxG58LgdL+VU7BeOSEvAQ==}
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
@@ -1327,6 +1394,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uc.micro@3.0.0:
|
||||||
|
resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==}
|
||||||
|
|
||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
@@ -2173,6 +2243,8 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.422: {}
|
electron-to-chromium@1.5.422: {}
|
||||||
|
|
||||||
|
entities@8.1.0: {}
|
||||||
|
|
||||||
esast-util-from-estree@2.0.0:
|
esast-util-from-estree@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree-jsx': 1.0.5
|
'@types/estree-jsx': 1.0.5
|
||||||
@@ -2360,6 +2432,10 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.33.0
|
lightningcss-win32-arm64-msvc: 1.33.0
|
||||||
lightningcss-win32-x64-msvc: 1.33.0
|
lightningcss-win32-x64-msvc: 1.33.0
|
||||||
|
|
||||||
|
linkify-it@6.1.0:
|
||||||
|
dependencies:
|
||||||
|
uc.micro: 3.0.0
|
||||||
|
|
||||||
linkifyjs@4.3.3: {}
|
linkifyjs@4.3.3: {}
|
||||||
|
|
||||||
lodash@4.18.1: {}
|
lodash@4.18.1: {}
|
||||||
@@ -2378,8 +2454,38 @@ snapshots:
|
|||||||
|
|
||||||
markdown-extensions@2.0.0: {}
|
markdown-extensions@2.0.0: {}
|
||||||
|
|
||||||
|
markdown-it-container@4.0.0: {}
|
||||||
|
|
||||||
|
markdown-it-footnote@4.0.0: {}
|
||||||
|
|
||||||
|
markdown-it-ins@4.0.0: {}
|
||||||
|
|
||||||
|
markdown-it-mark@4.0.0: {}
|
||||||
|
|
||||||
|
markdown-it-sup@2.0.0: {}
|
||||||
|
|
||||||
|
markdown-it-task-checkbox@1.0.6: {}
|
||||||
|
|
||||||
|
markdown-it-ts@1.1.2:
|
||||||
|
dependencies:
|
||||||
|
entities: 8.1.0
|
||||||
|
linkify-it: 6.1.0
|
||||||
|
mdurl: 2.1.0
|
||||||
|
punycode.js: 2.3.1
|
||||||
|
|
||||||
markdown-table@3.0.4: {}
|
markdown-table@3.0.4: {}
|
||||||
|
|
||||||
|
markstream-core@2.0.8: {}
|
||||||
|
|
||||||
|
markstream-react@2.0.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.8.0
|
||||||
|
clsx: 2.1.1
|
||||||
|
markstream-core: 2.0.8
|
||||||
|
react: 19.2.8
|
||||||
|
react-dom: 19.2.8(react@19.2.8)
|
||||||
|
stream-markdown-parser: 1.2.14
|
||||||
|
|
||||||
mdast-util-find-and-replace@3.0.2:
|
mdast-util-find-and-replace@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
@@ -2543,6 +2649,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
|
|
||||||
|
mdurl@2.1.0: {}
|
||||||
|
|
||||||
memoize-one@5.2.1: {}
|
memoize-one@5.2.1: {}
|
||||||
|
|
||||||
micromark-core-commonmark@2.0.3:
|
micromark-core-commonmark@2.0.3:
|
||||||
@@ -2931,6 +3039,8 @@ snapshots:
|
|||||||
prosemirror-state: 1.4.4
|
prosemirror-state: 1.4.4
|
||||||
prosemirror-transform: 1.12.1
|
prosemirror-transform: 1.12.1
|
||||||
|
|
||||||
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
react-dom@19.2.8(react@19.2.8):
|
react-dom@19.2.8(react@19.2.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
@@ -3078,6 +3188,16 @@ snapshots:
|
|||||||
|
|
||||||
space-separated-tokens@2.0.2: {}
|
space-separated-tokens@2.0.2: {}
|
||||||
|
|
||||||
|
stream-markdown-parser@1.2.14:
|
||||||
|
dependencies:
|
||||||
|
markdown-it-container: 4.0.0
|
||||||
|
markdown-it-footnote: 4.0.0
|
||||||
|
markdown-it-ins: 4.0.0
|
||||||
|
markdown-it-mark: 4.0.0
|
||||||
|
markdown-it-sup: 2.0.0
|
||||||
|
markdown-it-task-checkbox: 1.0.6
|
||||||
|
markdown-it-ts: 1.1.2
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
@@ -3112,6 +3232,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
uc.micro@3.0.0: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici@6.28.1: {}
|
undici@6.28.1: {}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { useChat } from "@ai-sdk/react";
|
import { useChat } from "@ai-sdk/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
|
lazy,
|
||||||
|
Suspense,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
Button,
|
Button,
|
||||||
@@ -35,6 +43,12 @@ import type {
|
|||||||
UIAction,
|
UIAction,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
const MessageMarkdown = lazy(() =>
|
||||||
|
import("./MessageMarkdown").then((module) => ({
|
||||||
|
default: module.MessageMarkdown,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
export function ChatPanel({
|
export function ChatPanel({
|
||||||
open,
|
open,
|
||||||
context,
|
context,
|
||||||
@@ -427,9 +441,25 @@ export function ChatPanel({
|
|||||||
</span>
|
</span>
|
||||||
{message.parts.map((part, index) =>
|
{message.parts.map((part, index) =>
|
||||||
part.type === "text" ? (
|
part.type === "text" ? (
|
||||||
<div className="ai-text" key={index}>
|
message.role === "assistant" ? (
|
||||||
{part.text}
|
<Suspense
|
||||||
</div>
|
key={index}
|
||||||
|
fallback={<div className="ai-text">{part.text}</div>}
|
||||||
|
>
|
||||||
|
<MessageMarkdown
|
||||||
|
content={part.text}
|
||||||
|
final={
|
||||||
|
!streaming ||
|
||||||
|
message.id !== chat.messages.at(-1)?.id ||
|
||||||
|
part.state === "done"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
) : (
|
||||||
|
<div className="ai-text" key={index}>
|
||||||
|
{part.text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
) : part.type === "data-tool" ? (
|
) : part.type === "data-tool" ? (
|
||||||
<BusinessCard
|
<BusinessCard
|
||||||
key={part.id ?? index}
|
key={part.id ?? index}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
import MarkdownRender from "markstream-react";
|
||||||
|
|
||||||
|
/** Render one assistant text part; final also settles interrupted streams. */
|
||||||
|
export const MessageMarkdown = memo(function MessageMarkdown({
|
||||||
|
content,
|
||||||
|
final,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
final: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="ai-markdown">
|
||||||
|
<MarkdownRender
|
||||||
|
content={content}
|
||||||
|
final={final}
|
||||||
|
fade={false}
|
||||||
|
htmlPolicy="escape"
|
||||||
|
renderCodeBlocksAsPre
|
||||||
|
batchRendering={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -82,6 +82,60 @@
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
.ai-markdown {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.ai-markdown .markstream-react {
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.ai-markdown :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
}
|
||||||
|
.ai-markdown :is(p, ul, ol, blockquote, pre, table) {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.ai-chat .ai-markdown .markstream-react pre[data-markstream-pre] {
|
||||||
|
font-size: 12px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--canvas);
|
||||||
|
}
|
||||||
|
.ai-markdown code {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.ai-markdown table {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.ai-markdown :is(th, td) {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ai-markdown a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.ai-markdown blockquote {
|
||||||
|
border-left: 3px solid var(--line);
|
||||||
|
padding-left: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
.ai-message.user .ai-text {
|
.ai-message.user .ai-text {
|
||||||
background: var(--canvas);
|
background: var(--canvas);
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "@douyinfe/semi-ui-19/react19-adapter";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import "markstream-react/index.css";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import "./ai/style.css";
|
import "./ai/style.css";
|
||||||
|
|
||||||
|
|||||||
@@ -269,3 +269,130 @@ test("Lark workspace keeps pagination, account details and chat usable at every
|
|||||||
fullPage: true,
|
fullPage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Markdown streams incomplete code, finishes safely and restores history", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
page.on("pageerror", (error) => errors.push(error.message));
|
||||||
|
await login(page);
|
||||||
|
await configure(page);
|
||||||
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
||||||
|
const chat = page.getByRole("complementary", { name: "AI 研究助手" });
|
||||||
|
await chat.getByRole("button", { name: "新会话", exact: true }).click();
|
||||||
|
const prefix = "## 研究结论\n\n**正在生成**\n\n```python\nrank(close)";
|
||||||
|
const suffix =
|
||||||
|
"\n```\n\n| 指标 | 值 |\n| --- | --- |\n| Sharpe | 1.2 |\n\n[文档](https://example.com)\n\n<script>alert('unsafe')</script>\n\n[危险](javascript:alert(1))";
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const original = window.fetch.bind(window);
|
||||||
|
window.fetch = async (url, init) => {
|
||||||
|
if (!String(url).match(/\/ai\/conversations\/[^/]+\/runs$/))
|
||||||
|
return original(url, init);
|
||||||
|
return new Response(
|
||||||
|
new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
Object.assign(window, {
|
||||||
|
pushMarkdown: (events: unknown[], done = false) => {
|
||||||
|
for (const event of events)
|
||||||
|
controller.enqueue(
|
||||||
|
new TextEncoder().encode(
|
||||||
|
`data: ${JSON.stringify(event)}\n\n`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (done) {
|
||||||
|
controller.enqueue(
|
||||||
|
new TextEncoder().encode("data: [DONE]\n\n"),
|
||||||
|
);
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/event-stream",
|
||||||
|
"x-vercel-ai-ui-message-stream": "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await chat
|
||||||
|
.getByRole("textbox", { name: "发送给研究助手" })
|
||||||
|
.fill("**用户原文**");
|
||||||
|
await chat.getByRole("button", { name: "发送", exact: true }).click();
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => typeof (window as any).pushMarkdown === "function",
|
||||||
|
);
|
||||||
|
await page.evaluate(
|
||||||
|
(delta) =>
|
||||||
|
(window as any).pushMarkdown([
|
||||||
|
{ type: "start", messageId: "markdown-answer" },
|
||||||
|
{ type: "text-start", id: "part" },
|
||||||
|
{ type: "text-delta", id: "part", delta },
|
||||||
|
]),
|
||||||
|
prefix,
|
||||||
|
);
|
||||||
|
await expect(chat.getByRole("heading", { name: "研究结论" })).toBeVisible();
|
||||||
|
await expect(chat.locator(".ai-markdown pre")).toContainText("rank(close)");
|
||||||
|
await expect(chat.locator(".ai-messages")).toHaveAttribute(
|
||||||
|
"aria-busy",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
await expect(chat.locator(".ai-message.user .ai-text")).toHaveText(
|
||||||
|
"**用户原文**",
|
||||||
|
);
|
||||||
|
await page.route("**/api/v1/ai/conversations/*", async (route) => {
|
||||||
|
if (route.request().method() !== "GET") return route.continue();
|
||||||
|
await route.fulfill({
|
||||||
|
json: {
|
||||||
|
id: route.request().url().split("/").at(-1),
|
||||||
|
title: "Markdown",
|
||||||
|
runs: [],
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: "markdown-answer",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [{ type: "text", text: prefix + suffix }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.evaluate(
|
||||||
|
(delta) =>
|
||||||
|
(window as any).pushMarkdown(
|
||||||
|
[
|
||||||
|
{ type: "text-delta", id: "part", delta },
|
||||||
|
{ type: "text-end", id: "part" },
|
||||||
|
{ type: "finish" },
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
suffix,
|
||||||
|
);
|
||||||
|
await expect(chat.locator(".ai-messages")).toHaveAttribute(
|
||||||
|
"aria-busy",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
await expect(chat.getByRole("cell", { name: "1.2" })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
chat.getByRole("link", { name: "Link: https://example.com", exact: true }),
|
||||||
|
).toHaveAttribute("href", "https://example.com");
|
||||||
|
await expect(
|
||||||
|
chat.locator(".ai-markdown script, .ai-markdown a[href^='javascript:']"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await page.setViewportSize({ width: 390, height: 900 });
|
||||||
|
expect(
|
||||||
|
await chat
|
||||||
|
.locator(".ai-messages")
|
||||||
|
.evaluate((el) => el.scrollWidth <= el.clientWidth),
|
||||||
|
).toBe(true);
|
||||||
|
await page.screenshot({ path: "../output/playwright/ai-markdown.png" });
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "打开研究助手" }).click();
|
||||||
|
await expect(chat.getByRole("heading", { name: "研究结论" })).toBeVisible();
|
||||||
|
await expect(chat.getByRole("cell", { name: "1.2" })).toBeVisible();
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user