Better parsing of pasted html as markdown so agents understand (#9190)

Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-05-14 12:57:18 -04:00
committed by GitHub
parent 4671053985
commit ffd2349b0e
3 changed files with 88 additions and 25 deletions
+2
View File
@@ -96,6 +96,7 @@
"swr": "^2.4.0",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"turndown": "^7.2.4",
"tw-animate-css": "^1.4.0",
"unist-util-visit": "^5.1.0",
"uuid": "^13.0.0",
@@ -132,6 +133,7 @@
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/shell-quote": "^1.7.5",
"@types/turndown": "^5.0.6",
"@types/yauzl": "^2.10.3",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
+54 -1
View File
@@ -44,6 +44,29 @@ import { UserInput, ImageData } from '../types/message';
import { compressImageDataUrl } from '../utils/conversionUtils';
import { fetchCanonicalModelInfo } from '../utils/canonical';
import { defineMessages, useIntl } from '../i18n';
import TurndownService from 'turndown';
const turndown = new TurndownService({
headingStyle: 'atx',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
});
turndown.addRule('complexLinks', {
filter: (node) => {
return (
node.nodeName === 'A' &&
!!node.getAttribute('href') &&
/\n/.test(node.textContent || '')
);
},
replacement: (content, node) => {
const el = node as HTMLElement;
const href = el.getAttribute('href')!;
const label = content.replace(/\n+/g, ' ').trim();
return `[${label}](${href})`;
},
});
interface PastedImage {
id: string;
@@ -805,10 +828,40 @@ export default function ChatInput({
}, [droppedFiles.length, localDroppedFiles.length, onFilesProcessed, setLocalDroppedFiles]);
const handlePaste = async (evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
if (isRecording) return;
const files = Array.from(evt.clipboardData.files || []);
const imageFiles = files.filter((file) => file.type.startsWith('image/'));
if (imageFiles.length === 0) return;
if (imageFiles.length === 0) {
const html = evt.clipboardData.getData('text/html');
if (html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const hasLinks = doc.querySelectorAll('a[href]').length > 0;
if (hasLinks) {
const markdown = turndown.turndown(doc.body).trim();
if (markdown) {
evt.preventDefault();
const textarea = textAreaRef.current;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newValue =
displayValue.substring(0, start) + markdown + displayValue.substring(end);
const cursorPos = start + markdown.length;
setDisplayValue(newValue);
updateValue(newValue);
setHasUserTyped(true);
checkForMentionOrSlash(newValue, cursorPos, textarea);
requestAnimationFrame(() => {
textarea.selectionStart = textarea.selectionEnd = cursorPos;
});
}
}
}
}
return;
}
// Check if adding these images would exceed the limit
if (pastedImages.length + imageFiles.length > MAX_IMAGES_PER_MESSAGE) {