feat(core): workspace attachment uploading & error (#12330)

### TL;DR

feat: optimize workspace attachment uploading & error display

![截屏2025-05-16 15.29.43.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/2408fe5e-e54d-44a8-882c-91e1b26bb660.png)

### What Changes
####
Support for Workspace Attachment Uploading & Error Handling
* Added support for three attachment states: uploading (local), upload failed (local error), and uploaded (persisted). The frontend UI now displays real-time upload progress and error messages.
* Attachments that fail to upload can be deleted directly without confirmation.
* Merged display of uploading and uploaded attachments for a smoother user experience.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Attachments now show real-time upload status including uploading, error, and uploaded states.
  - Users can remove failed (error) attachments instantly without confirmation.
  - Attachment list merges uploading and uploaded files, displaying up to 10 items.
- **Bug Fixes**
  - Improved error handling and messaging for failed attachment uploads.
- **Style**
  - Enhanced visual styling for error attachments with distinct colors and backgrounds.
- **Tests**
  - Added tests simulating slow network uploads, upload failures, and direct removal of error attachments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
yoyoyohamapi
2025-05-22 02:35:03 +00:00
parent 21ea65edc5
commit 45ed9038b6
8 changed files with 346 additions and 64 deletions

View File

@@ -82,15 +82,42 @@ test.describe('AISettings/Embedding', () => {
buffer: buffer2,
},
];
const client = await page.context().newCDPSession(page);
await client.send('Network.enable');
await client.send('Network.emulateNetworkConditions', {
offline: false,
latency: 1000,
downloadThroughput: (50 * 1024) / 8,
uploadThroughput: (50 * 1024) / 8,
connectionType: 'cellular3g',
});
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = await page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
// Uploading
await expect(
attachmentList.getByTestId(
'workspace-embedding-setting-attachment-uploading-item'
)
).toHaveCount(2);
// Persisted
await expect(
attachmentList.getByTestId('workspace-embedding-setting-attachment-item')
).toHaveCount(2);
await client.send('Network.emulateNetworkConditions', {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
});
await utils.settings.closeSettingsPanel(page);
await page.waitForTimeout(5000); // wait for the embedding to be ready
@@ -120,6 +147,36 @@ test.describe('AISettings/Embedding', () => {
}).toPass({ timeout: 20000 });
});
test('should display failed info if upload attachment failed', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.enableWorkspaceEmbedding(page);
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from('HelloWorld'),
},
];
await page.context().setOffline(true);
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = await page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
const errorItem = await attachmentList.getByTestId(
'workspace-embedding-setting-attachment-error-item'
);
await errorItem.hover();
await expect(page.getByText(/Network error/i)).toBeVisible();
await page.context().setOffline(false);
});
test('should support hybrid search for both globally uploaded attachments and those uploaded in the current session', async ({
loggedInPage: page,
utils,
@@ -216,7 +273,7 @@ test.describe('AISettings/Embedding', () => {
).toHaveText('document1.txt');
});
test('should support remove attachment', async ({
test('should support remove attachment with confirm', async ({
loggedInPage: page,
utils,
}) => {
@@ -240,6 +297,25 @@ test.describe('AISettings/Embedding', () => {
await utils.settings.removeAttachment(page, 'document1.txt');
});
test('should support remove error attachment directly', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.enableWorkspaceEmbedding(page);
const textContent = 'WorkspaceEBEEE is a cute cat';
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from(textContent),
},
];
await page.context().setOffline(true);
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
await utils.settings.removeAttachment(page, 'document1.txt', false);
await page.context().setOffline(false);
});
// FIXME: wait for indexer
test.skip('should support ignore docs for embedding', async ({
loggedInPage: page,

View File

@@ -87,23 +87,35 @@ export class SettingsPanelUtils {
while (count > 0) {
const attachmentItem = await page.getByTestId(itemId).first();
const hasErrorItem = await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-error-item')
.isVisible();
await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-delete-button')
.click();
await page.getByTestId('confirm-modal-confirm').click();
if (!hasErrorItem) {
await page.getByTestId('confirm-modal-confirm').click();
}
await page.waitForTimeout(1000);
count = await page.getByTestId(itemId).count();
}
}
public static async removeAttachment(page: Page, attachment: string) {
public static async removeAttachment(
page: Page,
attachment: string,
shouldConfirm = true
) {
const attachmentItem = await page
.getByTestId('workspace-embedding-setting-attachment-item')
.filter({ hasText: attachment });
await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-delete-button')
.click();
await page.getByTestId('confirm-modal-confirm').click();
if (shouldConfirm) {
await page.getByTestId('confirm-modal-confirm').click();
}
await page
.getByTestId('workspace-embedding-setting-attachment-item')
.filter({ hasText: attachment })