diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 4694c338e..b68a7e8bc 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1081,6 +1081,7 @@ jobs: - 'packages/backend/server/src/plugins/copilot/**' - 'packages/backend/server/tests/copilot.*' - 'packages/frontend/core/src/blocksuite/ai/**' + - 'packages/frontend/core/src/modules/workspace-indexer-embedding/**' - 'tests/affine-cloud-copilot/**' - name: Setup Node.js diff --git a/blocksuite/affine/components/src/icons/file-icons-rc.ts b/blocksuite/affine/components/src/icons/file-icons-rc.ts new file mode 100644 index 000000000..60a64db05 --- /dev/null +++ b/blocksuite/affine/components/src/icons/file-icons-rc.ts @@ -0,0 +1,173 @@ +import { + FileIconAepIcon, + FileIconAiIcon, + FileIconAviIcon, + FileIconCssIcon, + FileIconCsvIcon, + FileIconDmgIcon, + FileIconDocIcon, + FileIconDocxIcon, + FileIconEpsIcon, + FileIconExeIcon, + FileIconFigIcon, + FileIconGifIcon, + FileIconHtmlIcon, + FileIconInddIcon, + FileIconJavaIcon, + FileIconJpegIcon, + FileIconJsIcon, + FileIconJsonIcon, + FileIconMkvIcon, + FileIconMp3Icon, + FileIconMp4Icon, + FileIconMpegIcon, + FileIconNoneIcon, + FileIconPdfIcon, + FileIconPngIcon, + FileIconPptIcon, + FileIconPptxIcon, + FileIconPsdIcon, + FileIconRarIcon, + FileIconRssIcon, + FileIconSqlIcon, + FileIconSvgIcon, + FileIconTiffIcon, + FileIconTxtIcon, + FileIconWavIcon, + FileIconWebpIcon, + FileIconXlsIcon, + FileIconXlsxIcon, + FileIconXmlIcon, + FileIconZipIcon, + ImageIcon, +} from '@blocksuite/icons/rc'; + +export function getAttachmentFileIconRC(filetype: string) { + switch (filetype) { + case 'img': + return ImageIcon; + case 'image/jpeg': + case 'jpg': + case 'jpeg': + return FileIconJpegIcon; + case 'image/png': + case 'png': + return FileIconPngIcon; + case 'image/webp': + case 'webp': + return FileIconWebpIcon; + case 'image/tiff': + case 'tiff': + return FileIconTiffIcon; + case 'image/gif': + case 'gif': + return FileIconGifIcon; + case 'image/svg': + case 'svg': + return FileIconSvgIcon; + case 'image/eps': + case 'eps': + return FileIconEpsIcon; + case 'application/pdf': + case 'pdf': + return FileIconPdfIcon; + case 'application/msword': + case 'application/x-iwork-pages-sffpages': + case 'doc': + return FileIconDocIcon; + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + case 'docx': + return FileIconDocxIcon; + case 'text/plain': + case 'txt': + return FileIconTxtIcon; + case 'csv': + return FileIconCsvIcon; + case 'application/vnd.ms-excel': + case 'xls': + return FileIconXlsIcon; + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + case 'application/x-iwork-numbers-sffnumbers': + case 'xlsx': + return FileIconXlsxIcon; + case 'application/vnd.ms-powerpoint': + case 'application/x-iwork-keynote-sffkeynote': + case 'ppt': + return FileIconPptIcon; + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + case 'pptx': + return FileIconPptxIcon; + case 'application/illustrator': + case 'fig': + return FileIconFigIcon; + case 'application/postscript': + case 'ai': + return FileIconAiIcon; + case 'application/vnd.adobe.photoshop': + case 'psd': + return FileIconPsdIcon; + case 'application/vnd.adobe.indesign': + case 'indd': + return FileIconInddIcon; + case 'application/vnd.adobe.afterfx': + case 'aep': + return FileIconAepIcon; + case 'audio/mpeg': + case 'audio/mp3': + case 'mp3': + return FileIconMp3Icon; + case 'audio/wav': + case 'wav': + return FileIconWavIcon; + case 'video/mpeg': + case 'mpeg': + return FileIconMpegIcon; + case 'video/mp4': + case 'mp4': + return FileIconMp4Icon; + case 'video/avi': + case 'avi': + return FileIconAviIcon; + case 'video/mkv': + case 'mkv': + return FileIconMkvIcon; + case 'text/html': + case 'html': + return FileIconHtmlIcon; + case 'text/css': + case 'css': + return FileIconCssIcon; + case 'application/rss+xml': + case 'rss': + return FileIconRssIcon; + case 'application/sql': + case 'sql': + return FileIconSqlIcon; + case 'application/javascript': + case 'js': + return FileIconJsIcon; + case 'application/json': + case 'json': + return FileIconJsonIcon; + case 'application/java': + case 'java': + return FileIconJavaIcon; + case 'application/xml': + case 'xml': + return FileIconXmlIcon; + case 'application/x-msdos-program': + case 'exe': + return FileIconExeIcon; + case 'application/x-apple-diskimage': + case 'dmg': + return FileIconDmgIcon; + case 'application/zip': + case 'zip': + return FileIconZipIcon; + case 'application/x-rar-compressed': + case 'rar': + return FileIconRarIcon; + default: + return FileIconNoneIcon; + } +} diff --git a/blocksuite/affine/components/src/icons/index.ts b/blocksuite/affine/components/src/icons/index.ts index b08a7a2e4..acce40c9c 100644 --- a/blocksuite/affine/components/src/icons/index.ts +++ b/blocksuite/affine/components/src/icons/index.ts @@ -1,5 +1,6 @@ export * from './ai.js'; export * from './file-icons.js'; +export * from './file-icons-rc'; export * from './import-export.js'; export * from './list.js'; export * from './loading.js'; diff --git a/packages/frontend/component/src/components/setting-components/setting-row.tsx b/packages/frontend/component/src/components/setting-components/setting-row.tsx index df0a10070..fefc639d5 100644 --- a/packages/frontend/component/src/components/setting-components/setting-row.tsx +++ b/packages/frontend/component/src/components/setting-components/setting-row.tsx @@ -5,7 +5,7 @@ import { settingRow } from './share.css'; export type SettingRowProps = PropsWithChildren<{ name: ReactNode; - desc: ReactNode; + desc?: ReactNode; style?: CSSProperties; onClick?: () => void; spreadCol?: boolean; @@ -41,7 +41,7 @@ export const SettingRow = ({ >
{name}
-
{desc}
+ {desc &&
{desc}
}
{spreadCol ?
{children}
: children} diff --git a/packages/frontend/component/src/components/setting-components/wrapper.tsx b/packages/frontend/component/src/components/setting-components/wrapper.tsx index 396b682af..cfdd5bd36 100644 --- a/packages/frontend/component/src/components/setting-components/wrapper.tsx +++ b/packages/frontend/component/src/components/setting-components/wrapper.tsx @@ -7,6 +7,7 @@ interface SettingWrapperProps { id?: string; title?: ReactNode; disabled?: boolean; + testId?: string; } export const SettingWrapper = ({ @@ -14,9 +15,14 @@ export const SettingWrapper = ({ title, children, disabled, + testId, }: PropsWithChildren) => { return ( -
+
{title ?
{title}
: null} {children}
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts index 0e4cd147c..01dc8dfdd 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts @@ -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 { diff --git a/packages/frontend/core/src/components/page-list/selector/selector-layout.tsx b/packages/frontend/core/src/components/page-list/selector/selector-layout.tsx index f8f5e4348..69ff4afd4 100644 --- a/packages/frontend/core/src/components/page-list/selector/selector-layout.tsx +++ b/packages/frontend/core/src/components/page-list/selector/selector-layout.tsx @@ -44,9 +44,10 @@ export const SelectorLayout = ({ ); return ( -
+
{actions ?? ( <> - + + + + {attachmentNodes.length > 0 && ( + + )} + + + + + + {ignoredDocNodes.length > 0 && ( + + )} + + ); +}; diff --git a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/ignored-docs.tsx b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/ignored-docs.tsx new file mode 100644 index 000000000..74802f663 --- /dev/null +++ b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/ignored-docs.tsx @@ -0,0 +1,91 @@ +import { Loading } from '@affine/component'; +import { DocsService } from '@affine/core/modules/doc'; +import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; +import { i18nTime } from '@affine/i18n'; +import { useLiveData, useService } from '@toeverything/infra'; +import type React from 'react'; + +import type { IgnoredDoc } from '../types'; +import { + docItem, + docItemIcon, + docItemInfo, + docItemTitle, + excludeDocsWrapper, +} from './styles-css'; + +interface IgnoredDocsProps { + ignoredDocs: IgnoredDoc[]; + isLoading: boolean; +} + +interface DocItemProps { + doc: IgnoredDoc; +} + +const DocItem: React.FC = ({ doc }) => { + const docDisplayService = useService(DocDisplayMetaService); + const docService = useService(DocsService); + const docTitle = docDisplayService.title$(doc.docId).value; + const DocIcon = docDisplayService.icon$(doc.docId).value; + const docRecord = useLiveData(docService.list.doc$(doc.docId)); + if (!docRecord) { + return null; + } + const updatedDate = docRecord.meta$.value.updatedDate; + const createdDate = docRecord.meta$.value.createDate; + + const updateDate = updatedDate + ? i18nTime(updatedDate, { + relative: true, + }) + : '-'; + const createDate = createdDate + ? i18nTime(createdDate, { + absolute: { + accuracy: 'day', + noYear: true, + }, + }) + : '-'; + + return ( +
+
+ + {docTitle} +
+ +
+ {updateDate} + {createDate} +
+ + {/*
+ {doc.createdAt} + +
*/} +
+ ); +}; + +export const IgnoredDocs: React.FC = ({ + ignoredDocs, + isLoading, +}) => { + return ( +
+ {isLoading ? ( + + ) : ( + ignoredDocs.map(doc => ) + )} +
+ ); +}; diff --git a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/index.tsx b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/index.tsx new file mode 100644 index 000000000..ed43d6614 --- /dev/null +++ b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/index.tsx @@ -0,0 +1,22 @@ +import { SettingHeader } from '@affine/component/setting-components'; +import { useI18n } from '@affine/i18n'; +import type React from 'react'; + +import { EmbeddingSettings } from './embedding-settings'; + +export const IndexerEmbeddingSettings: React.FC = () => { + const t = useI18n(); + + return ( + <> + + + + + ); +}; diff --git a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/styles-css.ts b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/styles-css.ts new file mode 100644 index 000000000..7d24b5b38 --- /dev/null +++ b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/styles-css.ts @@ -0,0 +1,100 @@ +import { css } from '@emotion/css'; +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; + +export const attachmentsWrapper = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + alignItems: 'center', + padding: '8px', + gap: '4px', + isolation: 'isolate', + border: `1px solid ${cssVar('borderColor')}`, + borderRadius: '8px', + flexGrow: 0, + marginBottom: '16px', +}); + +export const attachmentItem = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: '4px', + gap: '4px', + border: `0.5px solid ${cssVar('borderColor')}`, + borderRadius: '4px', + flex: 'none', + alignSelf: 'stretch', + flexGrow: 0, +}); + +export const attachmentTitle = css({ + fontSize: '14px', + fontWeight: 400, + color: cssVar('textPrimaryColor'), + display: 'flex', + alignItems: 'center', + gap: '4px', +}); + +export const attachmentOperation = css({ + display: 'flex', + alignItems: 'center', +}); + +export const excludeDocsWrapper = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + alignItems: 'center', + padding: '8px', + gap: '4px', + isolation: 'isolate', + border: `1px solid ${cssVar('borderColor')}`, + borderRadius: '8px', + flexGrow: 0, + marginBottom: '8px', + overflowY: 'auto', + maxHeight: '508px', +}); + +export const docItem = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + alignSelf: 'stretch', + padding: '4px', + borderRadius: '4px', + cursor: 'pointer', + selectors: { + '&:hover': { + backgroundColor: cssVar('hoverColor'), + }, + }, +}); + +export const docItemTitle = css({ + fontSize: '14px', + fontWeight: 500, + color: cssVar('textPrimaryColor'), + display: 'flex', + alignItems: 'center', +}); + +export const docItemIcon = css({ + width: '20px', + height: '20px', + marginRight: '8px', + color: cssVarV2('icon/primary'), +}); + +export const docItemInfo = css({ + display: 'flex', + fontSize: '12px', + fontWeight: 400, + color: cssVar('textSecondaryColor'), + gap: '12px', + alignItems: 'center', +}); diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 121e0e2f9..387e88f48 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,26 +1,26 @@ { - "ar": 96, + "ar": 95, "ca": 4, "da": 4, - "de": 96, - "el-GR": 96, + "de": 95, + "el-GR": 95, "en": 100, - "es-AR": 96, + "es-AR": 95, "es-CL": 97, - "es": 96, - "fa": 96, - "fr": 96, + "es": 95, + "fa": 95, + "fr": 95, "hi": 2, - "it-IT": 96, + "it-IT": 95, "it": 1, - "ja": 96, + "ja": 95, "ko": 55, - "pl": 96, - "pt-BR": 96, - "ru": 96, - "sv-SE": 96, - "uk": 96, + "pl": 95, + "pt-BR": 95, + "ru": 95, + "sv-SE": 95, + "uk": 95, "ur": 2, - "zh-Hans": 96, - "zh-Hant": 96 + "zh-Hans": 95, + "zh-Hant": 95 } diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 69e19dcfd..b84d63d5d 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -6187,6 +6187,58 @@ export function useAFFiNEI18N(): { date: string; time: string; }>): string; + /** + * `Indexer & Embedding` + */ + ["com.affine.settings.workspace.indexer-embedding.title"](): string; + /** + * `Manage AFFiNE indexing and AFFiNE AI Embedding for local content processing` + */ + ["com.affine.settings.workspace.indexer-embedding.description"](): string; + /** + * `Embedding` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.title"](): string; + /** + * `Embedding allows AI to retrieve your content. If the indexer uses local settings, it may affect some of the results of the Embedding.` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.description"](): string; + /** + * `Select doc` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.select-doc"](): string; + /** + * `Workspace Embedding` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.switch.title"](): string; + /** + * `AI can call files embedded in the workspace.` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.switch.description"](): string; + /** + * `Ignore Docs` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.ignore-docs.title"](): string; + /** + * `The Ignored docs will not be embedded into the current workspace.` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.ignore-docs.description"](): string; + /** + * `Additional attachments` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.title"](): string; + /** + * `The uploaded file will be embedded in the current workspace.` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.description"](): string; + /** + * `Remove the attachment from embedding?` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.remove-attachment.title"](): string; + /** + * `Attachment will be removed. AI will not continue to extract content from this attachment.` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.remove-attachment.description"](): string; /** * `Sharing doc requires AFFiNE Cloud.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 310e6e520..f5ca73463 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1546,6 +1546,19 @@ "com.affine.settings.workspace.backup.import": "Enable local workspace", "com.affine.settings.workspace.backup.import.success.action": "Open", "com.affine.settings.workspace.backup.delete-at": "Deleted on {{date}} at {{time}}", + "com.affine.settings.workspace.indexer-embedding.title": "Indexer & Embedding", + "com.affine.settings.workspace.indexer-embedding.description": "Manage AFFiNE indexing and AFFiNE AI Embedding for local content processing", + "com.affine.settings.workspace.indexer-embedding.embedding.title": "Embedding", + "com.affine.settings.workspace.indexer-embedding.embedding.description": "Embedding allows AI to retrieve your content. If the indexer uses local settings, it may affect some of the results of the Embedding.", + "com.affine.settings.workspace.indexer-embedding.embedding.select-doc": "Select doc", + "com.affine.settings.workspace.indexer-embedding.embedding.switch.title": "Workspace Embedding", + "com.affine.settings.workspace.indexer-embedding.embedding.switch.description": "AI can call files embedded in the workspace.", + "com.affine.settings.workspace.indexer-embedding.embedding.ignore-docs.title": "Ignore Docs", + "com.affine.settings.workspace.indexer-embedding.embedding.ignore-docs.description": "The Ignored docs will not be embedded into the current workspace.", + "com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.title": "Additional attachments", + "com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.description": "The uploaded file will be embedded in the current workspace.", + "com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.remove-attachment.title": "Remove the attachment from embedding?", + "com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.remove-attachment.description": "Attachment will be removed. AI will not continue to extract content from this attachment.", "com.affine.share-menu.EnableCloudDescription": "Sharing doc requires AFFiNE Cloud.", "com.affine.share-menu.ShareMode": "Share mode", "com.affine.share-menu.SharePage": "Share doc", diff --git a/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts b/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts index e244a1d18..a385b0a11 100644 --- a/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts +++ b/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts @@ -4,7 +4,7 @@ import { test } from '../base/base-test'; test.describe.configure({ mode: 'serial' }); -test.describe.skip('AISettings/Embedding', () => { +test.describe('AISettings/Embedding', () => { test.beforeEach(async ({ loggedInPage: page, utils }) => { await utils.testUtils.setupTestEnvironment(page); await utils.chatPanel.openChatPanel(page);