fix: unify show cards rendering and prompt constraints
This commit is contained in:
parent
24c702c0af
commit
9199f5ca7c
1505
package-lock.json
generated
1505
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,13 +13,22 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/html2canvas": "^0.5.35",
|
"@types/html2canvas": "^0.5.35",
|
||||||
"diff": "^8.0.3",
|
"diff": "^8.0.3",
|
||||||
|
"dompurify": "^3.2.7",
|
||||||
"enquirer": "^2.4.1",
|
"enquirer": "^2.4.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"marked": "^11.1.0",
|
"marked": "^11.1.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
|
"rehype-stringify": "^10.0.1",
|
||||||
|
"remark-breaks": "^4.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-rehype": "^11.1.2",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
|
"unified": "^11.0.5",
|
||||||
"vue": "^3.4.15",
|
"vue": "^3.4.15",
|
||||||
"vue-stick-to-bottom": "^1.0.0"
|
"vue-stick-to-bottom": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -38,20 +38,20 @@
|
|||||||
|
|
||||||
## 3. 具体功能细节
|
## 3. 具体功能细节
|
||||||
|
|
||||||
### 3.1 图片展示
|
### 3.1 卡片展示(图片卡片 / HTML 卡片)
|
||||||
|
|
||||||
`<show_image>` 与 `<show_html>` 是**文本输出标签**,不是工具(tool)。
|
`<show_image>` 与 `<show_html>` 统一称为“卡片标签”,是**文本输出标签**,不是工具(tool)。
|
||||||
**严禁**把它们当作工具调用;只允许在最终回复正文中按规定格式直接输出标签。
|
**严禁**把它们当作工具调用;只允许在最终回复正文中按规定格式直接输出标签。
|
||||||
|
|
||||||
输出规则(必须遵守):
|
卡片输出规则(必须遵守):
|
||||||
- 标签本体不要放在代码块(```)里
|
- 标签本体不要放在代码块(```)里
|
||||||
- 标签本体前后不要再写“`<show_html>` / `<show_image>`”字样说明,避免误解析为嵌套或普通文本
|
- 标签本体前后不要再写“`<show_html>` / `<show_image>`”字样说明,避免误解析为嵌套或普通文本
|
||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
- `js` 必填:
|
- `js` 必填:
|
||||||
- `js="off"`:默认模式(建议优先使用),仅 HTML + CSS,支持流式实时渲染
|
- `js="off"`:默认模式(建议优先使用),仅 HTML + CSS,支持流式实时渲染
|
||||||
@ -67,7 +67,11 @@
|
|||||||
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px;需主动使用合适的间距调整、合适的元素大小和距离(如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
|
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px;需主动使用合适的间距调整、合适的元素大小和距离(如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
|
||||||
- 当 `js="off"` 时:标签内容只写 **HTML + CSS**,不要写 JavaScript;禁止输出 `<script>`、`onclick` 等事件脚本
|
- 当 `js="off"` 时:标签内容只写 **HTML + CSS**,不要写 JavaScript;禁止输出 `<script>`、`onclick` 等事件脚本
|
||||||
- 当 `js="on"` 时:允许输出 JavaScript(含 `<script>`),但应只包含与当前展示直接相关的最小脚本逻辑
|
- 当 `js="on"` 时:允许输出 JavaScript(含 `<script>`),但应只包含与当前展示直接相关的最小脚本逻辑
|
||||||
- 当用户说“给我画一个xxx”这类可视化诉求时,优先使用 `<show_html>` 标签直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
|
- **禁止写页面级样式**:不要在 HTML 卡片里写 `html` / `body` 选择器(例如 `html, body {{ ... }}`)
|
||||||
|
- **禁止使用视口高度布局**:不要写 `height: 100vh`、`min-height: 100vh`、`100dvh` 等整页高度写法
|
||||||
|
- **禁止双滚动容器**:不要同时给多层容器设置 `overflow: auto/scroll`;默认只让卡片外层负责滚动
|
||||||
|
- 需要铺满卡片时,请只给内部根容器(如 `.container`)使用 `min-height: 100%`,不要用 `100vh`
|
||||||
|
- 当用户说“给我画一个xxx”这类可视化诉求时,优先使用 HTML 卡片(`<show_html>`)直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
|
||||||
|
|
||||||
简单示例(正确格式):
|
简单示例(正确格式):
|
||||||
<show_html ratio="16:9" js="off">
|
<show_html ratio="16:9" js="off">
|
||||||
@ -75,7 +79,7 @@
|
|||||||
<style>.card{{padding:16px;border:1px solid #ddd;border-radius:10px}}</style>
|
<style>.card{{padding:16px;border:1px solid #ddd;border-radius:10px}}</style>
|
||||||
</show_html>
|
</show_html>
|
||||||
|
|
||||||
**图片检索流程**:Wikimedia Commons 优先 → `web_search` 全网搜索 → 校验匹配 → `<show_image>` 展示
|
**图片检索流程**:Wikimedia Commons 优先 → `web_search` 全网搜索 → 校验匹配 → 用 `<show_image>` 作为图片卡片展示
|
||||||
|
|
||||||
### 3.2 文件操作
|
### 3.2 文件操作
|
||||||
|
|
||||||
|
|||||||
@ -38,20 +38,20 @@
|
|||||||
|
|
||||||
## 3. 具体功能细节
|
## 3. 具体功能细节
|
||||||
|
|
||||||
### 3.1 图片展示
|
### 3.1 卡片展示(图片卡片 / HTML 卡片)
|
||||||
|
|
||||||
`<show_image>` 与 `<show_html>` 是**文本输出标签**,不是工具(tool)。
|
`<show_image>` 与 `<show_html>` 统一称为“卡片标签”,是**文本输出标签**,不是工具(tool)。
|
||||||
**严禁**把它们当作工具调用;只允许在最终回复正文中按规定格式直接输出标签。
|
**严禁**把它们当作工具调用;只允许在最终回复正文中按规定格式直接输出标签。
|
||||||
|
|
||||||
输出规则(必须遵守):
|
卡片输出规则(必须遵守):
|
||||||
- 标签本体不要放在代码块(```)里
|
- 标签本体不要放在代码块(```)里
|
||||||
- 标签本体前后不要再写“`<show_html>` / `<show_image>`”字样说明,避免误解析为嵌套或普通文本
|
- 标签本体前后不要再写“`<show_html>` / `<show_image>`”字样说明,避免误解析为嵌套或普通文本
|
||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
- `js` 必填:
|
- `js` 必填:
|
||||||
- `js="off"`:默认模式(建议优先使用),仅 HTML + CSS,支持流式实时渲染
|
- `js="off"`:默认模式(建议优先使用),仅 HTML + CSS,支持流式实时渲染
|
||||||
@ -67,7 +67,11 @@
|
|||||||
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px;需主动使用合适的间距调整、合适的元素大小和距离(如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
|
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px;需主动使用合适的间距调整、合适的元素大小和距离(如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
|
||||||
- 当 `js="off"` 时:标签内容只写 **HTML + CSS**,不要写 JavaScript;禁止输出 `<script>`、`onclick` 等事件脚本
|
- 当 `js="off"` 时:标签内容只写 **HTML + CSS**,不要写 JavaScript;禁止输出 `<script>`、`onclick` 等事件脚本
|
||||||
- 当 `js="on"` 时:允许输出 JavaScript(含 `<script>`),但应只包含与当前展示直接相关的最小脚本逻辑
|
- 当 `js="on"` 时:允许输出 JavaScript(含 `<script>`),但应只包含与当前展示直接相关的最小脚本逻辑
|
||||||
- 当用户说“给我画一个xxx”这类可视化诉求时,优先使用 `<show_html>` 标签直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
|
- **禁止写页面级样式**:不要在 HTML 卡片里写 `html` / `body` 选择器(例如 `html, body {{ ... }}`)
|
||||||
|
- **禁止使用视口高度布局**:不要写 `height: 100vh`、`min-height: 100vh`、`100dvh` 等整页高度写法
|
||||||
|
- **禁止双滚动容器**:不要同时给多层容器设置 `overflow: auto/scroll`;默认只让卡片外层负责滚动
|
||||||
|
- 需要铺满卡片时,请只给内部根容器(如 `.container`)使用 `min-height: 100%`,不要用 `100vh`
|
||||||
|
- 当用户说“给我画一个xxx”这类可视化诉求时,优先使用 HTML 卡片(`<show_html>`)直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
|
||||||
|
|
||||||
简单示例(正确格式):
|
简单示例(正确格式):
|
||||||
<show_html ratio="16:9" js="off">
|
<show_html ratio="16:9" js="off">
|
||||||
@ -75,7 +79,7 @@
|
|||||||
<style>.card{{padding:16px;border:1px solid #ddd;border-radius:10px}}</style>
|
<style>.card{{padding:16px;border:1px solid #ddd;border-radius:10px}}</style>
|
||||||
</show_html>
|
</show_html>
|
||||||
|
|
||||||
**图片检索流程**:Wikimedia Commons 优先 → `web_search` 全网搜索 → 校验匹配 → `<show_image>` 展示
|
**图片检索流程**:Wikimedia Commons 优先 → `web_search` 全网搜索 → 校验匹配 → 用 `<show_image>` 作为图片卡片展示
|
||||||
|
|
||||||
### 3.2 文件操作
|
### 3.2 文件操作
|
||||||
|
|
||||||
@ -107,7 +111,7 @@
|
|||||||
#### 实操建议(你要主动想到并执行)
|
#### 实操建议(你要主动想到并执行)
|
||||||
- **裁切策略**:优先裁"目标本体 + 周边上下文"而不是只裁最小块;读文字时再裁更紧
|
- **裁切策略**:优先裁"目标本体 + 周边上下文"而不是只裁最小块;读文字时再裁更紧
|
||||||
- **输出路径**:建议输出到 `/workspace/cache/` 或项目内临时目录(如 `cache/`),文件名带序号(例如 `crop_01.png`)
|
- **输出路径**:建议输出到 `/workspace/cache/` 或项目内临时目录(如 `cache/`),文件名带序号(例如 `crop_01.png`)
|
||||||
- **复核展示**:需要让用户/自己确认时,可用 `<show_image src="..." />` 展示裁切结果;或将裁切图作为本地文件再用 `view_image` 查看
|
- **复核展示**:需要让用户/自己确认时,可用 `<show_image src="..." />` 作为图片卡片展示裁切结果;或将裁切图作为本地文件再用 `view_image` 查看
|
||||||
- **多图对比**:同一部位若存在多张版本(原裁切/增强后),按顺序展示并说明"哪张更利于读字/看细节"
|
- **多图对比**:同一部位若存在多张版本(原裁切/增强后),按顺序展示并说明"哪张更利于读字/看细节"
|
||||||
|
|
||||||
### 3.4 终端操作
|
### 3.4 终端操作
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
let showImageRenderSeq = 0;
|
let showImageRenderSeq = 0;
|
||||||
let showImageDebugLogCount = 0;
|
let showImageDebugLogCount = 0;
|
||||||
@ -104,7 +105,7 @@ function summarizeNodeForDebug(node: Node | null) {
|
|||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
const el = node as Element;
|
const el = node as Element;
|
||||||
const tag = el.tagName.toLowerCase();
|
const tag = el.tagName.toLowerCase();
|
||||||
if (tag === 'show_image') {
|
if (tag === 'show_image' || tag === 'show-image') {
|
||||||
return `SHOW_IMAGE(src=${el.getAttribute('src') || ''})`;
|
return `SHOW_IMAGE(src=${el.getAttribute('src') || ''})`;
|
||||||
}
|
}
|
||||||
return `${tag.toUpperCase()}`;
|
return `${tag.toUpperCase()}`;
|
||||||
@ -124,9 +125,9 @@ function collectShowImageOrder(root: ParentNode | null = document) {
|
|||||||
} else if (current.nodeType === Node.ELEMENT_NODE) {
|
} else if (current.nodeType === Node.ELEMENT_NODE) {
|
||||||
const el = current as Element;
|
const el = current as Element;
|
||||||
const tag = el.tagName.toLowerCase();
|
const tag = el.tagName.toLowerCase();
|
||||||
if (tag === 'show_image') {
|
if (tag === 'show_image' || tag === 'show-image') {
|
||||||
tokens.push(`SHOW_IMAGE(${el.getAttribute('src') || ''})`);
|
tokens.push(`SHOW_IMAGE(${el.getAttribute('src') || ''})`);
|
||||||
} else if (tag === 'show_html') {
|
} else if (tag === 'show_html' || tag === 'show-html') {
|
||||||
tokens.push(`SHOW_HTML(ratio=${readShowHtmlRatio(el)},js=${readShowHtmlJsMode(el)})`);
|
tokens.push(`SHOW_HTML(ratio=${readShowHtmlRatio(el)},js=${readShowHtmlJsMode(el)})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,8 +247,8 @@ function computeAdaptiveShowHtmlSize(node: Element, ratioKey: string) {
|
|||||||
const widthScale = limitWidth / base.width;
|
const widthScale = limitWidth / base.width;
|
||||||
const heightScale = limitHeight / base.height;
|
const heightScale = limitHeight / base.height;
|
||||||
const scale = Math.min(1, widthScale, heightScale);
|
const scale = Math.min(1, widthScale, heightScale);
|
||||||
let widthPx = Math.max(220, Math.round(base.width * scale));
|
const widthPx = Math.max(220, Math.round(base.width * scale));
|
||||||
let heightPx = Math.round(widthPx / ratio);
|
const heightPx = Math.round(widthPx / ratio);
|
||||||
return { widthPx, heightPx, ratio };
|
return { widthPx, heightPx, ratio };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,20 +284,15 @@ function rewriteCssSelectorsForShadowDom(css: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeShowHtmlContent(rawHtml: string) {
|
function sanitizeShowHtmlContent(rawHtml: string) {
|
||||||
const parser = new DOMParser();
|
const purified = DOMPurify.sanitize(rawHtml || '', {
|
||||||
const doc = parser.parseFromString(`<body>${rawHtml || ''}</body>`, 'text/html');
|
USE_PROFILES: { html: true, svg: true, svgFilters: true, mathMl: true },
|
||||||
|
FORBID_TAGS: ['script', 'noscript', 'iframe', 'object', 'embed', 'link'],
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
ALLOW_ARIA_ATTR: true
|
||||||
|
}) as string;
|
||||||
|
|
||||||
const blockedSelectors = [
|
const parser = new DOMParser();
|
||||||
'script',
|
const doc = parser.parseFromString(`<body>${purified}</body>`, 'text/html');
|
||||||
'noscript',
|
|
||||||
'iframe',
|
|
||||||
'object',
|
|
||||||
'embed',
|
|
||||||
'link[rel="stylesheet"]'
|
|
||||||
];
|
|
||||||
blockedSelectors.forEach((selector) => {
|
|
||||||
doc.querySelectorAll(selector).forEach((el) => el.remove());
|
|
||||||
});
|
|
||||||
|
|
||||||
const elements = Array.from(doc.body.querySelectorAll('*'));
|
const elements = Array.from(doc.body.querySelectorAll('*'));
|
||||||
elements.forEach((el) => {
|
elements.forEach((el) => {
|
||||||
@ -352,6 +348,22 @@ function buildShowHtmlIframeSrcdoc(rawHtml: string) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(121, 109, 94, 0.48) transparent;
|
||||||
|
}
|
||||||
|
html::-webkit-scrollbar, body::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
html::-webkit-scrollbar-track, body::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
html::-webkit-scrollbar-thumb, body::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(121, 109, 94, 0.48);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
html::-webkit-scrollbar-thumb:hover, body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(121, 109, 94, 0.62);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -373,8 +385,12 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
const debugMode = getShowImageDebugMode();
|
const debugMode = getShowImageDebugMode();
|
||||||
const verbose = debugMode === 'verbose';
|
const verbose = debugMode === 'verbose';
|
||||||
const renderId = ++showImageRenderSeq;
|
const renderId = ++showImageRenderSeq;
|
||||||
const imageNodes = Array.from(root.querySelectorAll('show_image:not([data-rendered])'));
|
const imageNodes = Array.from(
|
||||||
const htmlNodes = Array.from(root.querySelectorAll('show_html:not([data-rendered])'));
|
root.querySelectorAll('show_image:not([data-rendered]), show-image:not([data-rendered])')
|
||||||
|
);
|
||||||
|
const htmlNodes = Array.from(
|
||||||
|
root.querySelectorAll('show_html:not([data-rendered]), show-html:not([data-rendered])')
|
||||||
|
);
|
||||||
if (htmlNodes.length > 0) {
|
if (htmlNodes.length > 0) {
|
||||||
markShowTagDrawingActive();
|
markShowTagDrawingActive();
|
||||||
}
|
}
|
||||||
@ -444,16 +460,16 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
const alt = node.getAttribute('alt') || '';
|
const alt = node.getAttribute('alt') || '';
|
||||||
const safeAlt = escapeHtml(alt.trim());
|
const safeAlt = escapeHtml(alt.trim());
|
||||||
const figure = document.createElement('figure');
|
const figure = document.createElement('figure');
|
||||||
figure.className = 'chat-inline-image';
|
figure.className = 'chat-inline-card chat-inline-card--image chat-inline-image';
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
img.src = mappedSrc;
|
img.src = mappedSrc;
|
||||||
img.alt = safeAlt;
|
img.alt = safeAlt;
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
figure.classList.add('chat-inline-image--error');
|
figure.classList.add('chat-inline-card--error', 'chat-inline-image--error');
|
||||||
const tip = document.createElement('div');
|
const tip = document.createElement('div');
|
||||||
tip.className = 'chat-inline-image__error';
|
tip.className = 'chat-inline-card__error chat-inline-image__error';
|
||||||
tip.textContent = '图片加载失败';
|
tip.textContent = '图片加载失败';
|
||||||
figure.appendChild(tip);
|
figure.appendChild(tip);
|
||||||
};
|
};
|
||||||
@ -461,6 +477,7 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
|
|
||||||
if (safeAlt) {
|
if (safeAlt) {
|
||||||
const caption = document.createElement('figcaption');
|
const caption = document.createElement('figcaption');
|
||||||
|
caption.className = 'chat-inline-card__caption';
|
||||||
caption.innerHTML = safeAlt;
|
caption.innerHTML = safeAlt;
|
||||||
figure.appendChild(caption);
|
figure.appendChild(caption);
|
||||||
}
|
}
|
||||||
@ -569,11 +586,12 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
let pending = showHtmlJsPendingRenderByPath.get(pathKey);
|
let pending = showHtmlJsPendingRenderByPath.get(pathKey);
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'chat-inline-html chat-inline-html--pending';
|
wrapper.className =
|
||||||
|
'chat-inline-card chat-inline-card--html chat-inline-card--pending chat-inline-html chat-inline-html--pending';
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
host.className = 'chat-inline-html__host chat-inline-html__host--pending';
|
host.className = 'chat-inline-card__body chat-inline-html__host chat-inline-html__host--pending';
|
||||||
const tip = document.createElement('div');
|
const tip = document.createElement('div');
|
||||||
tip.className = 'chat-inline-html__pending-tip';
|
tip.className = 'chat-inline-card__pending-tip chat-inline-html__pending-tip';
|
||||||
tip.textContent = '正在渲染内容...';
|
tip.textContent = '正在渲染内容...';
|
||||||
host.appendChild(tip);
|
host.appendChild(tip);
|
||||||
wrapper.appendChild(host);
|
wrapper.appendChild(host);
|
||||||
@ -611,7 +629,8 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
let persistent = showHtmlJsIframeRenderByPath.get(pathKey);
|
let persistent = showHtmlJsIframeRenderByPath.get(pathKey);
|
||||||
if (!persistent) {
|
if (!persistent) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'chat-inline-html chat-inline-html--iframe';
|
wrapper.className =
|
||||||
|
'chat-inline-card chat-inline-card--html chat-inline-card--iframe chat-inline-html chat-inline-html--iframe';
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.className = 'chat-inline-html__iframe';
|
iframe.className = 'chat-inline-html__iframe';
|
||||||
iframe.setAttribute('sandbox', 'allow-scripts');
|
iframe.setAttribute('sandbox', 'allow-scripts');
|
||||||
@ -730,9 +749,9 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
}
|
}
|
||||||
if (!persistent) {
|
if (!persistent) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'chat-inline-html';
|
wrapper.className = 'chat-inline-card chat-inline-card--html chat-inline-html';
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
host.className = 'chat-inline-html__host';
|
host.className = 'chat-inline-card__body chat-inline-html__host';
|
||||||
const shadow = host.attachShadow({ mode: 'open' });
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
@ -757,6 +776,22 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(121, 109, 94, 0.48) transparent;
|
||||||
|
}
|
||||||
|
.show-html-root::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
.show-html-root::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.show-html-root::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(121, 109, 94, 0.48);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.show-html-root::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(121, 109, 94, 0.62);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const root = document.createElement('div');
|
const root = document.createElement('div');
|
||||||
@ -1138,12 +1173,15 @@ export function setupShowImageObserver() {
|
|||||||
const hasShowImageRelatedMutation = mutations.some((mutation) => {
|
const hasShowImageRelatedMutation = mutations.some((mutation) => {
|
||||||
const targetIsShowImage =
|
const targetIsShowImage =
|
||||||
mutation.target instanceof Element &&
|
mutation.target instanceof Element &&
|
||||||
['show_image', 'show_html'].includes(mutation.target.tagName.toLowerCase());
|
['show_image', 'show_html', 'show-image', 'show-html'].includes(
|
||||||
|
mutation.target.tagName.toLowerCase()
|
||||||
|
);
|
||||||
const addedHasShowTag = Array.from(mutation.addedNodes).some(
|
const addedHasShowTag = Array.from(mutation.addedNodes).some(
|
||||||
(n) =>
|
(n) =>
|
||||||
n instanceof Element &&
|
n instanceof Element &&
|
||||||
(['show_image', 'show_html'].includes(n.tagName.toLowerCase()) ||
|
(['show_image', 'show_html', 'show-image', 'show-html'].includes(
|
||||||
!!n.querySelector?.('show_image,show_html'))
|
n.tagName.toLowerCase()
|
||||||
|
) || !!n.querySelector?.('show_image,show_html,show-image,show-html'))
|
||||||
);
|
);
|
||||||
return targetIsShowImage || addedHasShowTag;
|
return targetIsShowImage || addedHasShowTag;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -764,7 +764,7 @@ function getScrollMetrics(el: HTMLElement) {
|
|||||||
height,
|
height,
|
||||||
client,
|
client,
|
||||||
remain: height - top - client,
|
remain: height - top - client,
|
||||||
hasShowHtml: !!el.querySelector('show_html, .chat-inline-html')
|
hasShowHtml: !!el.querySelector('show_html, show-html, .chat-inline-html, .chat-inline-card--html')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import { marked } from 'marked';
|
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import renderMathInElement from 'katex/contrib/auto-render';
|
import renderMathInElement from 'katex/contrib/auto-render';
|
||||||
|
import { unified } from 'unified';
|
||||||
|
import remarkParse from 'remark-parse';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkBreaks from 'remark-breaks';
|
||||||
|
import remarkRehype from 'remark-rehype';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||||
|
import rehypeStringify from 'rehype-stringify';
|
||||||
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
let latexRenderTimer: number | null = null;
|
let latexRenderTimer: number | null = null;
|
||||||
const markdownCache = new Map<string, string>();
|
const markdownCache = new Map<string, string>();
|
||||||
@ -144,6 +152,7 @@ function transformShowHtmlBlocks(raw: string, isStreaming: boolean) {
|
|||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
const closeTag = '</show_html>';
|
const closeTag = '</show_html>';
|
||||||
|
const closeTagForMarkdown = '</show-html>';
|
||||||
|
|
||||||
while (cursor < raw.length) {
|
while (cursor < raw.length) {
|
||||||
const start = raw.indexOf('<show_html', cursor);
|
const start = raw.indexOf('<show_html', cursor);
|
||||||
@ -178,14 +187,14 @@ function transformShowHtmlBlocks(raw: string, isStreaming: boolean) {
|
|||||||
jsMode,
|
jsMode,
|
||||||
partialLength: partialOpen.length
|
partialLength: partialOpen.length
|
||||||
});
|
});
|
||||||
output += `<show_html${ratioAttr}${jsAttr} data-partial="1">${closeTag}`;
|
output += `<show-html${ratioAttr}${jsAttr} data-partial="1">${closeTagForMarkdown}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const openTag = raw.slice(start, openEnd + 1);
|
const openTag = raw.slice(start, openEnd + 1);
|
||||||
const ratio = extractShowHtmlRatio(openTag);
|
const ratio = extractShowHtmlRatio(openTag);
|
||||||
const jsMode = extractShowHtmlJsMode(openTag);
|
const jsMode = extractShowHtmlJsMode(openTag);
|
||||||
const canonicalOpenTag = `<show_html ratio="${ratio}" js="${jsMode}">`;
|
const canonicalOpenTag = `<show-html ratio="${ratio}" js="${jsMode}">`;
|
||||||
const closeStart = raw.indexOf(closeTag, openEnd + 1);
|
const closeStart = raw.indexOf(closeTag, openEnd + 1);
|
||||||
if (closeStart === -1) {
|
if (closeStart === -1) {
|
||||||
if (!isStreaming) {
|
if (!isStreaming) {
|
||||||
@ -206,7 +215,7 @@ function transformShowHtmlBlocks(raw: string, isStreaming: boolean) {
|
|||||||
/>$/,
|
/>$/,
|
||||||
encoded ? ` data-encoded="${encoded}" data-partial="1">` : ' data-partial="1">'
|
encoded ? ` data-encoded="${encoded}" data-partial="1">` : ' data-partial="1">'
|
||||||
);
|
);
|
||||||
output += `${safeOpen}${closeTag}`;
|
output += `${safeOpen}${closeTagForMarkdown}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +230,7 @@ function transformShowHtmlBlocks(raw: string, isStreaming: boolean) {
|
|||||||
encodedLength: encoded.length
|
encodedLength: encoded.length
|
||||||
});
|
});
|
||||||
const safeOpen = canonicalOpenTag.replace(/>$/, encoded ? ` data-encoded="${encoded}">` : '>');
|
const safeOpen = canonicalOpenTag.replace(/>$/, encoded ? ` data-encoded="${encoded}">` : '>');
|
||||||
output += `${safeOpen}${closeTag}`;
|
output += `${safeOpen}${closeTagForMarkdown}`;
|
||||||
cursor = closeStart + closeTag.length;
|
cursor = closeStart + closeTag.length;
|
||||||
}
|
}
|
||||||
showHtmlDebugLog('md:transform:end', {
|
showHtmlDebugLog('md:transform:end', {
|
||||||
@ -233,6 +242,124 @@ function transformShowHtmlBlocks(raw: string, isStreaming: boolean) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transformShowImageBlocks(raw: string) {
|
||||||
|
if (!raw || raw.indexOf('show_image') === -1) return raw;
|
||||||
|
let output = '';
|
||||||
|
let cursor = 0;
|
||||||
|
const openToken = '<show_image';
|
||||||
|
const closeToken = '</show_image>';
|
||||||
|
|
||||||
|
while (cursor < raw.length) {
|
||||||
|
const openIdx = raw.indexOf(openToken, cursor);
|
||||||
|
const closeIdx = raw.indexOf(closeToken, cursor);
|
||||||
|
if (openIdx === -1 && closeIdx === -1) {
|
||||||
|
output += raw.slice(cursor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const hasOpen = openIdx !== -1;
|
||||||
|
const hasClose = closeIdx !== -1;
|
||||||
|
const useOpen = hasOpen && (!hasClose || openIdx < closeIdx);
|
||||||
|
const hitIdx = useOpen ? openIdx : closeIdx;
|
||||||
|
if (hitIdx < 0) {
|
||||||
|
output += raw.slice(cursor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isShowHtmlTagInsideMarkdownCode(raw, hitIdx)) {
|
||||||
|
output += raw.slice(cursor, hitIdx + 1);
|
||||||
|
cursor = hitIdx + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output += raw.slice(cursor, hitIdx);
|
||||||
|
if (useOpen) {
|
||||||
|
output += '<show-image';
|
||||||
|
cursor = hitIdx + openToken.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output += '</show-image>';
|
||||||
|
cursor = hitIdx + closeToken.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RehypeProps = Record<string, unknown>;
|
||||||
|
|
||||||
|
function readAttr(props: RehypeProps | undefined, name: string): string {
|
||||||
|
if (!props) return '';
|
||||||
|
const direct = props[name];
|
||||||
|
if (typeof direct === 'string') return direct;
|
||||||
|
const camel = name.replace(/-([a-z])/g, (_, ch: string) => ch.toUpperCase());
|
||||||
|
const camelVal = props[camel];
|
||||||
|
if (typeof camelVal === 'string') return camelVal;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeAttr(props: RehypeProps, name: string, value: string | null) {
|
||||||
|
const camel = name.replace(/-([a-z])/g, (_, ch: string) => ch.toUpperCase());
|
||||||
|
if (value == null || value === '') {
|
||||||
|
delete props[name];
|
||||||
|
delete props[camel];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeShowTagsPlugin() {
|
||||||
|
return (tree: any) => {
|
||||||
|
visit(tree, 'element', (node: any) => {
|
||||||
|
if (!node || typeof node.tagName !== 'string') return;
|
||||||
|
if (node.tagName === 'show_html' || node.tagName === 'show-html') {
|
||||||
|
node.tagName = 'show_html';
|
||||||
|
const props = (node.properties || {}) as RehypeProps;
|
||||||
|
const ratio = normalizeShowHtmlRatio(readAttr(props, 'ratio'));
|
||||||
|
const jsMode = normalizeShowHtmlJsMode(readAttr(props, 'js'));
|
||||||
|
const encoded = readAttr(props, 'data-encoded');
|
||||||
|
const partial = readAttr(props, 'data-partial');
|
||||||
|
writeAttr(props, 'ratio', ratio);
|
||||||
|
writeAttr(props, 'js', jsMode);
|
||||||
|
writeAttr(props, 'data-encoded', encoded);
|
||||||
|
writeAttr(props, 'data-partial', partial === '1' ? '1' : null);
|
||||||
|
node.properties = props;
|
||||||
|
}
|
||||||
|
if (node.tagName === 'show_image' || node.tagName === 'show-image') {
|
||||||
|
node.tagName = 'show_image';
|
||||||
|
const props = (node.properties || {}) as RehypeProps;
|
||||||
|
const src = readAttr(props, 'src').trim();
|
||||||
|
const alt = readAttr(props, 'alt').trim();
|
||||||
|
const title = readAttr(props, 'title').trim();
|
||||||
|
writeAttr(props, 'src', src);
|
||||||
|
writeAttr(props, 'alt', alt || title || '');
|
||||||
|
if (!title) {
|
||||||
|
writeAttr(props, 'title', null);
|
||||||
|
}
|
||||||
|
node.properties = props;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedSchema: Record<string, any> = {
|
||||||
|
...defaultSchema,
|
||||||
|
tagNames: [...(defaultSchema.tagNames || []), 'show_html', 'show_image', 'show-html', 'show-image'],
|
||||||
|
attributes: {
|
||||||
|
...(defaultSchema.attributes || {}),
|
||||||
|
show_html: ['ratio', 'js', 'data-encoded', 'data-partial'],
|
||||||
|
show_image: ['src', 'alt', 'title', 'width', 'height'],
|
||||||
|
'show-html': ['ratio', 'js', 'data-encoded', 'data-partial'],
|
||||||
|
'show-image': ['src', 'alt', 'title', 'width', 'height']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const markdownProcessor = unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkGfm)
|
||||||
|
.use(remarkBreaks)
|
||||||
|
.use(remarkRehype, { allowDangerousHtml: true })
|
||||||
|
.use(rehypeRaw)
|
||||||
|
.use(normalizeShowTagsPlugin)
|
||||||
|
.use(rehypeSanitize, sanitizedSchema)
|
||||||
|
.use(rehypeStringify, { allowDangerousHtml: false });
|
||||||
|
|
||||||
function stableHash(input: string): string {
|
function stableHash(input: string): string {
|
||||||
let hash = 5381;
|
let hash = 5381;
|
||||||
for (let i = 0; i < input.length; i += 1) {
|
for (let i = 0; i < input.length; i += 1) {
|
||||||
@ -275,20 +402,25 @@ function wrapCodeBlocks(html: string, isStreaming = false) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMarkdownToHtml(text: string): string {
|
||||||
|
const file = markdownProcessor.processSync(text);
|
||||||
|
return String(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(input: string) {
|
||||||
|
return input
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
export function renderMarkdown(text: string, isStreaming = false) {
|
export function renderMarkdown(text: string, isStreaming = false) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof marked === 'undefined') {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
breaks: true,
|
|
||||||
gfm: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isStreaming) {
|
if (!isStreaming) {
|
||||||
const cacheKey = buildCacheKey(text);
|
const cacheKey = buildCacheKey(text);
|
||||||
if (markdownCache.has(cacheKey)) {
|
if (markdownCache.has(cacheKey)) {
|
||||||
@ -296,9 +428,14 @@ export function renderMarkdown(text: string, isStreaming = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeText = transformShowHtmlBlocks(text, isStreaming);
|
const safeText = transformShowImageBlocks(transformShowHtmlBlocks(text, isStreaming));
|
||||||
const parsed = marked.parse(safeText) as string;
|
let html = '';
|
||||||
let html = parsed;
|
try {
|
||||||
|
html = renderMarkdownToHtml(safeText);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Markdown 渲染失败,降级为纯文本渲染:', error);
|
||||||
|
html = escapeHtml(safeText).replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
html = wrapCodeBlocks(html, isStreaming);
|
html = wrapCodeBlocks(html, isStreaming);
|
||||||
|
|
||||||
if (!isStreaming && text.length < 10000) {
|
if (!isStreaming && text.length < 10000) {
|
||||||
|
|||||||
@ -101,7 +101,7 @@ function getMessagesAreaMetrics(messagesArea: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLastShowHtmlMetrics(messagesArea: HTMLElement) {
|
function getLastShowHtmlMetrics(messagesArea: HTMLElement) {
|
||||||
const list = messagesArea.querySelectorAll('.chat-inline-html');
|
const list = messagesArea.querySelectorAll('.chat-inline-card--html, .chat-inline-html');
|
||||||
const last = (list[list.length - 1] as HTMLElement) || null;
|
const last = (list[list.length - 1] as HTMLElement) || null;
|
||||||
if (!last) return null;
|
if (!last) return null;
|
||||||
const areaRect = messagesArea.getBoundingClientRect();
|
const areaRect = messagesArea.getBoundingClientRect();
|
||||||
@ -117,7 +117,9 @@ function getLastShowHtmlMetrics(messagesArea: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasShowHtmlNodeInArea(messagesArea: HTMLElement) {
|
function hasShowHtmlNodeInArea(messagesArea: HTMLElement) {
|
||||||
return !!messagesArea.querySelector('show_html, .chat-inline-html');
|
return !!messagesArea.querySelector(
|
||||||
|
'show_html, show-html, .chat-inline-html, .chat-inline-card--html'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHtmlTraceLog(
|
function showHtmlTraceLog(
|
||||||
|
|||||||
@ -889,15 +889,61 @@ body[data-theme='dark'] .more-icon {
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-image {
|
.chat-inline-card {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 14px auto;
|
||||||
|
border: 1px solid var(--claude-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: var(--claude-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-inline-card__body {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-inline-card--pending {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-inline-card__pending-tip {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-inline-card__error {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-inline-card__caption {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-inline-card--image {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin: 14px 0;
|
margin: 14px 0;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-image img {
|
.chat-inline-card--image img {
|
||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
@ -910,50 +956,15 @@ body[data-theme='dark'] .more-icon {
|
|||||||
background: var(--claude-card);
|
background: var(--claude-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-image--error {
|
.chat-inline-card--error {
|
||||||
border: 1px dashed var(--claude-accent);
|
border: 1px dashed var(--claude-accent);
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--claude-highlight);
|
background: var(--claude-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-image__error {
|
.chat-inline-card--html {
|
||||||
font-size: 12px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-inline-image figcaption {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.4;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-inline-html {
|
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
|
||||||
margin: 14px auto;
|
|
||||||
border: 1px solid var(--claude-border);
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: var(--claude-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-inline-html__host {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-inline-html--pending {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-html__host--pending {
|
.chat-inline-html__host--pending {
|
||||||
@ -977,8 +988,13 @@ body[data-theme='dark'] .more-icon {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 兼容旧类名(可逐步移除) */
|
||||||
|
.chat-inline-image__error { @extend .chat-inline-card__error; }
|
||||||
|
.chat-inline-image figcaption { @extend .chat-inline-card__caption; }
|
||||||
|
|
||||||
/* show_html 在 streaming 重建期间会短暂回到原始标签,需提供稳定占位避免 scrollHeight 抖动 */
|
/* show_html 在 streaming 重建期间会短暂回到原始标签,需提供稳定占位避免 scrollHeight 抖动 */
|
||||||
show_html:not([data-rendered="1"]) {
|
show_html:not([data-rendered="1"]),
|
||||||
|
show-html:not([data-rendered="1"]) {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -992,11 +1008,16 @@ show_html:not([data-rendered="1"]) {
|
|||||||
box-shadow: var(--claude-shadow);
|
box-shadow: var(--claude-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
show_html:not([data-rendered="1"])[ratio="1:1"] { aspect-ratio: 1 / 1; }
|
show_html:not([data-rendered="1"])[ratio="1:1"],
|
||||||
show_html:not([data-rendered="1"])[ratio="16:9"] { aspect-ratio: 16 / 9; }
|
show-html:not([data-rendered="1"])[ratio="1:1"] { aspect-ratio: 1 / 1; }
|
||||||
show_html:not([data-rendered="1"])[ratio="9:16"] { aspect-ratio: 9 / 16; }
|
show_html:not([data-rendered="1"])[ratio="16:9"],
|
||||||
show_html:not([data-rendered="1"])[ratio="4:3"] { aspect-ratio: 4 / 3; }
|
show-html:not([data-rendered="1"])[ratio="16:9"] { aspect-ratio: 16 / 9; }
|
||||||
show_html:not([data-rendered="1"])[ratio="3:4"] { aspect-ratio: 3 / 4; }
|
show_html:not([data-rendered="1"])[ratio="9:16"],
|
||||||
|
show-html:not([data-rendered="1"])[ratio="9:16"] { aspect-ratio: 9 / 16; }
|
||||||
|
show_html:not([data-rendered="1"])[ratio="4:3"],
|
||||||
|
show-html:not([data-rendered="1"])[ratio="4:3"] { aspect-ratio: 4 / 3; }
|
||||||
|
show_html:not([data-rendered="1"])[ratio="3:4"],
|
||||||
|
show-html:not([data-rendered="1"])[ratio="3:4"] { aspect-ratio: 3 / 4; }
|
||||||
|
|
||||||
.text-output .text-content {
|
.text-output .text-content {
|
||||||
padding: 0 20px 0 15px;
|
padding: 0 20px 0 15px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user