feat(core): workspace embedding settings (#11801)

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

- **New Features**
  - Introduced "Indexer & Embedding" workspace settings to manage AI embedding for local content, including document ignoring and attachment uploads.
  - Added UI components for embedding settings, attachments, and ignored documents with pagination and deletion capabilities.
  - Provided comprehensive file-type icons for attachments.

- **Improvements**
  - Added a new tab for indexing and embedding in workspace settings navigation.
  - Included test identifiers on key UI elements to enhance automated testing.

- **Localization**
  - Added English localization strings covering all embedding-related UI text and actions.

- **Bug Fixes**
  - Enabled previously skipped end-to-end tests for embedding settings to improve reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
yoyoyohamapi
2025-05-15 09:36:28 +00:00
parent 6224344a4f
commit 6c9f28e08b
25 changed files with 1266 additions and 86 deletions
@@ -20,17 +20,8 @@ import { ChatAbortIcon, ChatSendIcon } from '../../_common/icons';
import { type AIError, AIProvider } from '../../provider';
import { reportResponse } from '../../utils/action-reporter';
import { readBlobAsURL } from '../../utils/image';
import type {
ChatChip,
DocDisplayConfig,
FileChip,
} from '../ai-chat-chips/type';
import {
isCollectionChip,
isDocChip,
isFileChip,
isTagChip,
} from '../ai-chat-chips/utils';
import type { ChatChip, DocDisplayConfig } from '../ai-chat-chips/type';
import { isDocChip } from '../ai-chat-chips/utils';
import type { ChatMessage } from '../ai-chat-messages';
import { MAX_IMAGE_COUNT } from './const';
import type {
@@ -588,7 +579,6 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
const sessionId = await this.createSessionId();
let contexts = await this._getMatchedContexts(userInput);
contexts = this._filterContexts(contexts);
if (abortController.signal.aborted) {
return;
}
@@ -673,9 +663,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
private async _getMatchedContexts(userInput: string) {
const contextId = await this.getContextId();
if (!contextId) {
return { files: [], docs: [] };
}
const workspaceId = this.host.store.workspace.id;
const docContexts = new Map<
string,
@@ -687,7 +675,11 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
>();
const { files: matchedFiles = [], docs: matchedDocs = [] } =
(await AIProvider.context?.matchContext(userInput, contextId)) ?? {};
(await AIProvider.context?.matchContext(
userInput,
contextId,
workspaceId
)) ?? {};
matchedDocs.forEach(doc => {
docContexts.set(doc.docId, {
@@ -701,17 +693,12 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
if (context) {
context.fileContent += `\n${file.content}`;
} else {
const fileChip = this.chips.find(
chip => isFileChip(chip) && chip.fileId === file.fileId
) as FileChip | undefined;
if (fileChip && fileChip.blobId) {
fileContexts.set(file.fileId, {
blobId: fileChip.blobId,
fileName: fileChip.file.name,
fileType: fileChip.file.type,
fileContent: file.content,
});
}
fileContexts.set(file.fileId, {
blobId: file.blobId,
fileName: file.name,
fileType: file.mimeType,
fileContent: file.content,
});
}
});
@@ -753,42 +740,6 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) {
files: Array.from(fileContexts.values()),
};
}
// TODO: remove this function after workspace embedding is ready
private _filterContexts(contexts: {
docs: BlockSuitePresets.AIDocContextOption[];
files: BlockSuitePresets.AIFileContextOption[];
}) {
const docIds = this.chips.reduce((acc, chip) => {
if (isDocChip(chip)) {
acc.push(chip.docId);
}
if (isTagChip(chip)) {
const docIds = this.docDisplayConfig.getTagPageIds(chip.tagId);
acc.push(...docIds);
}
if (isCollectionChip(chip)) {
const docIds = this.docDisplayConfig.getCollectionPageIds(
chip.collectionId
);
acc.push(...docIds);
}
return acc;
}, [] as string[]);
const fileIds = this.chips.reduce((acc, chip) => {
if (isFileChip(chip) && chip.blobId) {
acc.push(chip.blobId);
}
return acc;
}, [] as string[]);
const { docs, files } = contexts;
return {
docs: docs.filter(doc => docIds.includes(doc.docId)),
files: files.filter(file => fileIds.includes(file.blobId)),
};
}
}
declare global {