⬆️ (fix): redo after rebase
✨(front) add e2e pasting an attachment from clipboard
add test e2e paste attachment
This commit is contained in:
@@ -8,6 +8,10 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(front) allow pasting an attachment from clipboard
|
||||
|
||||
### Fixed
|
||||
|
||||
- 💚(docker) vendor mime.types file instead of fetching from Apache SVN
|
||||
|
||||
@@ -213,39 +213,56 @@ export const InputChat = ({
|
||||
}
|
||||
}, [status, input]);
|
||||
|
||||
const handleFilesAccepted = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
setFiles((prev) => {
|
||||
const dt = new DataTransfer();
|
||||
const validateAndAddFiles = useCallback(
|
||||
(filesToAdd: File[]) => {
|
||||
const acceptedFiles: File[] = [];
|
||||
const rejectedFiles: File[] = [];
|
||||
|
||||
// Keep existing files
|
||||
if (prev) {
|
||||
Array.from(prev).forEach((f) => dt.items.add(f));
|
||||
filesToAdd.forEach((file) => {
|
||||
if (isFileAccepted(file)) {
|
||||
acceptedFiles.push(file);
|
||||
} else {
|
||||
rejectedFiles.push(file);
|
||||
}
|
||||
|
||||
// Add new files (avoiding duplicates)
|
||||
acceptedFiles.forEach((f) => {
|
||||
const isDuplicate = Array.from(prev || []).some(
|
||||
(pf) =>
|
||||
pf.name === f.name &&
|
||||
pf.size === f.size &&
|
||||
pf.lastModified === f.lastModified,
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
dt.items.add(f);
|
||||
}
|
||||
});
|
||||
|
||||
return dt.files;
|
||||
});
|
||||
|
||||
if (rejectedFiles.length > 0) {
|
||||
showToastError();
|
||||
}
|
||||
|
||||
if (acceptedFiles.length > 0) {
|
||||
setFiles((prev) => {
|
||||
const dt = new DataTransfer();
|
||||
|
||||
// Keep existing files
|
||||
if (prev) {
|
||||
Array.from(prev).forEach((f) => dt.items.add(f));
|
||||
}
|
||||
|
||||
// Add new files (avoiding duplicates)
|
||||
acceptedFiles.forEach((f) => {
|
||||
const isDuplicate = Array.from(prev || []).some(
|
||||
(pf) =>
|
||||
pf.name === f.name &&
|
||||
pf.size === f.size &&
|
||||
pf.lastModified === f.lastModified,
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
dt.items.add(f);
|
||||
}
|
||||
});
|
||||
|
||||
return dt.files;
|
||||
});
|
||||
}
|
||||
},
|
||||
[setFiles],
|
||||
[isFileAccepted, showToastError, setFiles],
|
||||
);
|
||||
|
||||
const { isDragActive } = useFileDragDrop({
|
||||
enabled: fileUploadEnabled,
|
||||
isFileAccepted,
|
||||
onFilesAccepted: handleFilesAccepted,
|
||||
onFilesAccepted: validateAndAddFiles,
|
||||
onFilesRejected: () => showToastError(),
|
||||
});
|
||||
|
||||
@@ -293,6 +310,41 @@ export const InputChat = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!fileUploadEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboardData = e.clipboardData;
|
||||
if (!clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Due to browser limitations, only one file can be pasted at a time
|
||||
// Check files first (for files from file system)
|
||||
let file: File | null = null;
|
||||
|
||||
if (clipboardData.files && clipboardData.files.length > 0) {
|
||||
file = clipboardData.files[0];
|
||||
} else if (clipboardData.items) {
|
||||
for (let i = 0; i < clipboardData.items.length; i++) {
|
||||
const item = clipboardData.items[i];
|
||||
if (item.kind === 'file') {
|
||||
file = item.getAsFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (file) {
|
||||
e.preventDefault();
|
||||
validateAndAddFiles([file]);
|
||||
}
|
||||
},
|
||||
[fileUploadEnabled, validateAndAddFiles],
|
||||
);
|
||||
|
||||
const handleAttachClick = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
@@ -309,46 +361,10 @@ export const InputChat = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const acceptedFiles: File[] = [];
|
||||
const rejectedFiles: string[] = [];
|
||||
|
||||
Array.from(fileList).forEach((file) => {
|
||||
if (isFileAccepted(file)) {
|
||||
acceptedFiles.push(file);
|
||||
} else {
|
||||
rejectedFiles.push(file.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (rejectedFiles.length > 0) {
|
||||
showToastError();
|
||||
}
|
||||
|
||||
if (acceptedFiles.length > 0) {
|
||||
setFiles((prev) => {
|
||||
const dt = new DataTransfer();
|
||||
if (prev) {
|
||||
Array.from(prev).forEach((f) => dt.items.add(f));
|
||||
}
|
||||
acceptedFiles.forEach((f) => {
|
||||
if (
|
||||
!Array.from(prev || []).some(
|
||||
(pf) =>
|
||||
pf.name === f.name &&
|
||||
pf.size === f.size &&
|
||||
pf.lastModified === f.lastModified,
|
||||
)
|
||||
) {
|
||||
dt.items.add(f);
|
||||
}
|
||||
});
|
||||
return dt.files;
|
||||
});
|
||||
}
|
||||
|
||||
validateAndAddFiles(Array.from(fileList));
|
||||
e.target.value = '';
|
||||
},
|
||||
[isFileAccepted, showToastError, setFiles],
|
||||
[validateAndAddFiles],
|
||||
);
|
||||
|
||||
const handleAttachmentRemove = useCallback(
|
||||
@@ -443,6 +459,7 @@ export const InputChat = ({
|
||||
name="inputchat-textarea"
|
||||
onChange={handleTextareaChange}
|
||||
onKeyDown={handleTextareaKeyDown}
|
||||
onPaste={handlePaste}
|
||||
disabled={isInputDisabled}
|
||||
rows={1}
|
||||
style={textareaStyle}
|
||||
|
||||
@@ -45,11 +45,6 @@ export const ToolInvocationItem: React.FC<ToolInvocationItemProps> = ({
|
||||
$width="100%"
|
||||
$maxWidth="750px"
|
||||
$margin={{ all: 'auto', top: 'base', bottom: 'md' }}
|
||||
$background="var(--c--contextuals--background--surface--tertiary)"
|
||||
$color="var(--c--contextuals--content--semantic--neutral--secondary)"
|
||||
$padding={{ all: 'sm' }}
|
||||
$radius="8px"
|
||||
$css="font-size: 0.9em; width: 100%; white-space: pre-wrap; word-wrap: break-word;"
|
||||
>
|
||||
<Loader />
|
||||
<Text $variation="600" $size="md">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { overrideConfig } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/home/');
|
||||
});
|
||||
@@ -69,4 +71,71 @@ test.describe('Chat page', () => {
|
||||
await chatHistoryLink.click();
|
||||
await expect(messageContent).toBeVisible();
|
||||
});
|
||||
|
||||
test('the user can paste a document into the chat input', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
FEATURE_FLAGS: {
|
||||
'document-upload': 'enabled',
|
||||
'web-search': 'enabled',
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const chatInput = page.getByRole('textbox', {
|
||||
name: 'Enter your message or a',
|
||||
});
|
||||
await expect(chatInput).toBeVisible();
|
||||
await chatInput.click();
|
||||
|
||||
// Create a test file content
|
||||
const fileContent = 'Test document content for paste';
|
||||
const fileName = 'test-document.txt';
|
||||
const fileType = 'text/plain';
|
||||
|
||||
// Simulate paste event with file
|
||||
await page.evaluate(
|
||||
({ content, name, type }) => {
|
||||
const textarea = document.querySelector(
|
||||
'textarea[name="inputchat-textarea"]',
|
||||
) as HTMLTextAreaElement;
|
||||
if (!textarea) return;
|
||||
|
||||
// Create a File object
|
||||
const file = new File([content], name, { type });
|
||||
|
||||
// Create a DataTransfer object to simulate clipboard
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
// Create a paste event - ClipboardEvent constructor doesn't accept clipboardData
|
||||
// so we create a regular Event and add clipboardData property
|
||||
const pasteEvent = new Event('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}) as unknown as ClipboardEvent;
|
||||
|
||||
// Define clipboardData property to make it accessible
|
||||
Object.defineProperty(pasteEvent, 'clipboardData', {
|
||||
value: {
|
||||
files: dataTransfer.files,
|
||||
items: dataTransfer.items,
|
||||
types: Array.from(dataTransfer.types),
|
||||
getData: () => '',
|
||||
setData: () => {},
|
||||
},
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
textarea.dispatchEvent(pasteEvent);
|
||||
},
|
||||
{ content: fileContent, name: fileName, type: fileType },
|
||||
);
|
||||
|
||||
// Wait for the file to be processed and appear in the attachment list
|
||||
// The attachment should be visible with the file name
|
||||
const attachment = page.getByText(fileName, { exact: false }).first();
|
||||
await expect(attachment).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user