🐛 fix(vscode): 代码阅读对话里的回答悬停链接时卡片一直闪、盖住链接:回答每次重画都新建 {__html},React 换了对象就整段重设 innerHTML,弹出定义卡片引起的重画把悬停着的链接换成新元素,卡片按脱离页面的旧链接落到左上角盖住链接,指针下的新链接又重新问卡片、再被换掉,来回闪;回答改用 MarkdownPreview,同一段 HTML 用同一个对象,链接不再被换
This commit is contained in:
1 parent
d6960b894c
commit
2841e2b477
3 files changed
+62
-7
No files matched your search
@@ -237,3 +237,56 @@ describe('复制回答', () => {
|
||||
expect(writeText).toHaveBeenCalledExactlyOnceWith('找到了 **3 条**路径')
|
||||
})
|
||||
})
|
||||
|
||||
describe('回答里的链接', () => {
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
||||
container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
container.remove()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
const ANSWER = '- [HomeShell.startSessionWith](src/app/home/HomeShell.tsx#HomeShell.startSessionWith):把首句暂存后跳到编程聊天页'
|
||||
|
||||
async function mount(props: Partial<AskThreadProps>): Promise<void> {
|
||||
await act(async () => root.render(<AskThread history={[]} asks={[]} open={false} profiles={PROFILES} now={NOW} onSelectProfile={() => {}} onAsk={() => {}} onStop={() => {}} onDelete={() => {}} onClose={() => {}} {...props} />))
|
||||
}
|
||||
|
||||
function link(): HTMLAnchorElement {
|
||||
const found = container.querySelector<HTMLAnchorElement>('.cr-qa-answer a')
|
||||
if (!found)
|
||||
throw new Error('回答里没有链接')
|
||||
return found
|
||||
}
|
||||
|
||||
it('展开的历史回答:页面为别的事重画(如弹出定义卡片)、传进等值的新对象时,还是原来那个链接,悬停的那处不被换掉', async () => {
|
||||
const history = (): QaEntryView[] => [{ ...HISTORY[0]!, answer: ANSWER }]
|
||||
await mount({ history: history() })
|
||||
await act(async () => container.querySelector<HTMLButtonElement>('[data-history] .cr-qa-question')!.click())
|
||||
const original = link()
|
||||
for (const now of [NOW + 1, NOW + 2])
|
||||
await mount({ history: history(), now })
|
||||
expect(link()).toBe(original)
|
||||
expect(original.isConnected).toBe(true)
|
||||
})
|
||||
|
||||
it('答完的当前一轮同样;回答的字变了才换成新的链接', async () => {
|
||||
await mount({ open: true, asks: [ask({ status: 'done', text: ANSWER })] })
|
||||
const original = link()
|
||||
await mount({ open: true, asks: [ask({ status: 'done', text: ANSWER })], now: NOW + 1 })
|
||||
expect(link()).toBe(original)
|
||||
expect(original.isConnected).toBe(true)
|
||||
await mount({ open: true, asks: [ask({ status: 'done', text: `${ANSWER}\n- 再往下` })] })
|
||||
expect(original.isConnected).toBe(false)
|
||||
expect(link().textContent).toBe('HomeShell.startSessionWith')
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ import { useCopyText } from '@/hooks/useCopyText'
|
||||
import { renderMarkdown, renderMarkdownOnce } from './markdown'
|
||||
import { answerPreview, relativeTime, unfinished } from './qa'
|
||||
import { MAX_IMAGES, pickImages, readImage, transferFiles } from './qaImages'
|
||||
import { MarkdownPreview } from './TextPreview'
|
||||
import { showInViewport } from './useReveal'
|
||||
|
||||
/** 一种对话在界面上怎么称呼:区域的名字、图标、「问过 N 次」、删一条、输入框的提示和名字、回答长出来时的状态 */
|
||||
@@ -157,7 +158,7 @@ function HistoryItem({ entry, wording, now, onDelete, onOpenImage }: { entry: Qa
|
||||
</div>
|
||||
<Thumbs urls={entry.images ?? []} onOpen={onOpenImage} />
|
||||
{expanded
|
||||
? <div className="cr-markdown cr-qa-answer" dangerouslySetInnerHTML={{ __html: renderMarkdown(entry.answer, 'answer') }} />
|
||||
? <MarkdownPreview html={renderMarkdown(entry.answer, 'answer')} className="cr-qa-answer" />
|
||||
: (
|
||||
<button type="button" className="cr-qa-preview" onClick={toggle}>
|
||||
{answerPreview(entry.answer)}
|
||||
@@ -187,10 +188,10 @@ function CurrentItem({ ask, wording, onOpenImage }: { ask: Ask, wording: ThreadW
|
||||
</div>
|
||||
<Thumbs urls={ask.images ?? []} onOpen={onOpenImage} />
|
||||
{ask.text !== '' && (
|
||||
<div
|
||||
className="cr-markdown cr-qa-answer"
|
||||
data-streaming={running ? '' : undefined}
|
||||
dangerouslySetInnerHTML={{ __html: running ? renderMarkdownOnce(ask.text) : renderMarkdown(ask.text, 'answer') }}
|
||||
<MarkdownPreview
|
||||
html={running ? renderMarkdownOnce(ask.text) : renderMarkdown(ask.text, 'answer')}
|
||||
className="cr-qa-answer"
|
||||
streaming={running}
|
||||
/>
|
||||
)}
|
||||
{running && (
|
||||
|
||||
@@ -25,7 +25,8 @@ export const TextPreview = memo(({ text, className, find }: { text: PreviewText,
|
||||
})
|
||||
TextPreview.displayName = 'TextPreview'
|
||||
|
||||
export function MarkdownPreview({ html, className, find }: { html: string, className?: string, find?: string }): ReactElement {
|
||||
export function MarkdownPreview({ html, className, find, streaming }: { html: string, className?: string, find?: string, streaming?: boolean }): ReactElement {
|
||||
// 同一段 HTML 用同一个对象:React 见到新对象就整段重设 innerHTML,里面的链接都换成新元素,悬停着的那个就脱离了页面
|
||||
const content = useMemo(() => ({ __html: html }), [html])
|
||||
return <div className={cn('cr-markdown', className)} data-find={find} dangerouslySetInnerHTML={content} />
|
||||
return <div className={cn('cr-markdown', className)} data-find={find} data-streaming={streaming ? '' : undefined} dangerouslySetInnerHTML={content} />
|
||||
}
|
||||
Reference in new issue
Block a user