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:
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user