Polish web chat presentation

This commit is contained in:
Yichen Jiang
2026-07-30 19:30:58 +08:00
parent 3921610f5b
commit b121adcf1a
23 changed files with 201 additions and 87 deletions
@@ -685,6 +685,18 @@ export const IconChecklistOutline14 = ({ size = 14, className }: IconProps) => (
</svg>
)
/** Text document glyph for context-disclosure rows. */
export const IconTextOutline14 = ({ size = 14, className }: IconProps) => (
<svg width={size} height={size} className={className} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.083 1.167h4.55c.31 0 .606.123.825.342l2.7 2.7c.219.219.342.516.342.825v6.05c0 .966-.784 1.75-1.75 1.75H4.083a1.75 1.75 0 0 1-1.75-1.75V2.917c0-.967.784-1.75 1.75-1.75Zm4.084 1.166H4.083a.583.583 0 0 0-.583.584v8.166c0 .323.261.584.583.584h6.667a.583.583 0 0 0 .583-.584V5.25H9.917a1.75 1.75 0 0 1-1.75-1.75V2.333Zm1.166.825 1.175 1.175h-.591a.583.583 0 0 1-.584-.583v-.592ZM5.25 7h4.667v1.167H5.25V7Zm0 2.333h4.667V10.5H5.25V9.333Z"
fill="currentColor"
/>
</svg>
)
/** ic_ds_List_Pen_outline_16 */
export const IconListPenOutline16 = ({ size = 16, className }: IconProps) => (
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -24,6 +24,7 @@ export { TerminalBlock, DEFAULT_TERMINAL_MAX_LINES } from './TerminalBlock.tsx'
export type { TerminalBlockProps } from './TerminalBlock.tsx'
export { CodeBlock } from './markdown/CodeBlock.tsx'
export { JsonBlock } from './markdown/JsonBlock.tsx'
export type { JsonBlockProps } from './markdown/JsonBlock.tsx'
export { MarkdownText } from './markdown/MarkdownText.tsx'
export { MessageText } from './markdown/MessageText.tsx'
export { extractMarkdownPlainText } from './markdown/plain-text.ts'
@@ -3,22 +3,54 @@
}
.toggle {
font-size: 12px;
line-height: 18px;
display: inline-flex;
align-items: center;
min-height: 24px;
font-size: 14px;
line-height: 24px;
color: var(--dsw-alias-label-secondary);
padding: 2px 6px;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
border-radius: 6px;
}
.toggle:hover {
background: var(--dsw-alias-interactive-bg-hover);
.leading {
position: relative;
flex: none;
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 6px;
color: var(--dsw-alias-label-tertiary);
}
.iconIdle {
display: inline-flex;
opacity: 1;
transition: opacity 100ms ease;
}
.chevronHover {
position: absolute;
inset: 0;
margin: auto;
opacity: 0;
transition: opacity 100ms ease;
}
.toggle:hover .iconIdle {
opacity: 0;
}
.toggle:hover .chevronHover {
opacity: 1;
}
.body {
margin: 4px 0 0;
margin: 4px 0 0 22px;
padding: 8px;
max-height: 200px;
overflow: auto;
@@ -1,15 +1,28 @@
// JsonBlock: collapsible JSON block (conversation side; independent from the RPC panel's PayloadJson to avoid cross-panel coupling).
// JsonBlock: accessible JSON disclosure row (conversation side; independent
// from the RPC panel's PayloadJson to avoid cross-panel coupling).
import { useMemo, useState } from 'react'
import { useMemo, useState, type ReactNode } from 'react'
import { IconChevronDownOutline14, IconChevronRightOutline14 } from '../icons/index.tsx'
import css from './JsonBlock.module.css'
const MAX_CHARS = 20_000
export function JsonBlock({ label, payload, defaultOpen = false }: {
/** Props for the compact JSON disclosure used in conversation content. */
export interface JsonBlockProps {
label: string
payload: unknown
defaultOpen?: boolean
}) {
/** Semantic glyph shown while collapsed; hover previews the disclosure chevron. */
collapsedIcon?: ReactNode
}
/** Render a bounded, pretty-printed JSON disclosure. */
export function JsonBlock({
label,
payload,
defaultOpen = false,
collapsedIcon,
}: JsonBlockProps) {
const [open, setOpen] = useState(defaultOpen)
const body = useMemo(() => {
if (!open) return ''
@@ -23,10 +36,26 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
}
return s.length > MAX_CHARS ? `${s.slice(0, MAX_CHARS)}\n… 已截断,共 ${s.length} 字符` : s
}, [open, payload])
const leading = open
? <IconChevronDownOutline14 />
: collapsedIcon === undefined
? <IconChevronRightOutline14 />
: (
<>
<span className={css.iconIdle}>{collapsedIcon}</span>
<IconChevronRightOutline14 className={css.chevronHover} />
</>
)
return (
<div className={css.root}>
<button type="button" className={css.toggle} onClick={() => { setOpen(v => !v) }}>
{open ? '▾' : '▸'} {label}
<button
type="button"
className={css.toggle}
aria-expanded={open}
onClick={() => { setOpen(v => !v) }}
>
<span className={css.leading} aria-hidden="true">{leading}</span>
<span>{label}</span>
</button>
{open && <pre className={css.body}>{body}</pre>}
</div>