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

View File

@@ -14,7 +14,7 @@ export type SettingTab =
| 'editor'
| 'account'
| 'meetings'
| `workspace:${'preference' | 'properties' | 'members' | 'storage' | 'billing' | 'license' | 'integrations'}`;
| `workspace:${'preference' | 'properties' | 'members' | 'storage' | 'billing' | 'license' | 'integrations' | 'indexer-embedding' | 'search'}`;
export type GLOBAL_DIALOG_SCHEMA = {
'create-workspace': (props: { serverId?: string }) => {

View File

@@ -58,6 +58,7 @@ import { configureThemeEditorModule } from './theme-editor';
import { configureUrlModule } from './url';
import { configureUserspaceModule } from './userspace';
import { configureWorkspaceModule } from './workspace';
import { configureIndexerEmbeddingModule } from './workspace-indexer-embedding';
import { configureWorkspacePropertyModule } from './workspace-property';
export function configureCommonModules(framework: Framework) {
@@ -116,4 +117,5 @@ export function configureCommonModules(framework: Framework) {
configureIntegrationModule(framework);
configureWorkspacePropertyModule(framework);
configureCollectionRulesModule(framework);
configureIndexerEmbeddingModule(framework);
}

View File

@@ -0,0 +1 @@
export const COUNT_PER_PAGE = 10;

View File

@@ -0,0 +1,240 @@
import { logger } from '@affine/core/modules/share-doc/entities/share-docs-list';
import type { WorkspaceService } from '@affine/core/modules/workspace';
import type { PaginationInput } from '@affine/graphql';
import {
catchErrorInto,
effect,
Entity,
fromPromise,
LiveData,
onComplete,
onStart,
smartRetry,
} from '@toeverything/infra';
import { EMPTY } from 'rxjs';
import { concatMap, exhaustMap, mergeMap } from 'rxjs/operators';
import { COUNT_PER_PAGE } from '../constants';
import type { EmbeddingStore } from '../stores/embedding';
import type { AttachmentFile, IgnoredDoc } from '../types';
export interface EmbeddingConfig {
enabled: boolean;
}
interface Attachments {
totalCount: number;
pageInfo: {
endCursor: string | null;
hasNextPage: boolean;
};
edges: {
node: AttachmentFile;
}[];
}
type IgnoredDocs = IgnoredDoc[];
export class Embedding extends Entity {
enabled$ = new LiveData<boolean>(false);
error$ = new LiveData<any>(null);
attachments$ = new LiveData<Attachments>({
edges: [],
pageInfo: {
endCursor: null,
hasNextPage: false,
},
totalCount: 0,
});
ignoredDocs$ = new LiveData<IgnoredDocs>([]);
isEnabledLoading$ = new LiveData(false);
isAttachmentsLoading$ = new LiveData(false);
isIgnoredDocsLoading$ = new LiveData(false);
constructor(
private readonly workspaceService: WorkspaceService,
private readonly store: EmbeddingStore
) {
super();
this.getEnabled();
this.getAttachments({ first: COUNT_PER_PAGE, after: null });
this.getIgnoredDocs();
}
getEnabled = effect(
exhaustMap(() => {
return fromPromise(signal =>
this.store.getEnabled(this.workspaceService.workspace.id, signal)
).pipe(
smartRetry(),
mergeMap(value => {
this.enabled$.next(value);
return EMPTY;
}),
catchErrorInto(this.error$, error => {
logger.error(
'Failed to fetch workspace doc embedding enabled',
error
);
}),
onStart(() => this.isEnabledLoading$.setValue(true)),
onComplete(() => this.isEnabledLoading$.setValue(false))
);
})
);
setEnabled = effect(
exhaustMap((enabled: boolean) => {
return fromPromise(signal =>
this.store.updateEnabled(
this.workspaceService.workspace.id,
enabled,
signal
)
).pipe(
smartRetry(),
concatMap(() => {
this.getEnabled();
return EMPTY;
}),
catchErrorInto(this.error$, error => {
logger.error(
'Failed to update workspace doc embedding enabled',
error
);
}),
onStart(() => this.isEnabledLoading$.setValue(true)),
onComplete(() => this.isEnabledLoading$.setValue(false))
);
})
);
getIgnoredDocs = effect(
exhaustMap(() => {
return fromPromise(signal =>
this.store.getIgnoredDocs(this.workspaceService.workspace.id, signal)
).pipe(
smartRetry(),
mergeMap(value => {
this.ignoredDocs$.next(value);
return EMPTY;
}),
catchErrorInto(this.error$, error => {
logger.error(
'Failed to fetch workspace doc embedding ignored docs',
error
);
}),
onStart(() => this.isIgnoredDocsLoading$.setValue(true)),
onComplete(() => this.isIgnoredDocsLoading$.setValue(false))
);
})
);
updateIgnoredDocs = effect(
exhaustMap(({ add, remove }: { add: string[]; remove: string[] }) => {
return fromPromise(signal =>
this.store.updateIgnoredDocs(
this.workspaceService.workspace.id,
add,
remove,
signal
)
).pipe(
smartRetry(),
concatMap(() => {
this.getIgnoredDocs();
return EMPTY;
}),
catchErrorInto(this.error$, error => {
logger.error(
'Failed to update workspace doc embedding ignored docs',
error
);
})
);
})
);
getAttachments = effect(
exhaustMap((pagination: PaginationInput) => {
return fromPromise(signal =>
this.store.getEmbeddingFiles(
this.workspaceService.workspace.id,
pagination,
signal
)
).pipe(
smartRetry(),
mergeMap(value => {
this.attachments$.next(value);
return EMPTY;
}),
catchErrorInto(this.error$, error => {
logger.error(
'Failed to fetch workspace doc embedding attachments',
error
);
}),
onStart(() => this.isAttachmentsLoading$.setValue(true)),
onComplete(() => this.isAttachmentsLoading$.setValue(false))
);
})
);
addAttachments = effect(
exhaustMap((files: File[]) => {
return fromPromise(signal =>
this.store.addEmbeddingFiles(
this.workspaceService.workspace.id,
files,
signal
)
).pipe(
concatMap(() => {
this.getAttachments({ first: COUNT_PER_PAGE, after: null });
return EMPTY;
}),
catchErrorInto(this.error$, error => {
logger.error(
'Failed to add workspace doc embedding attachments',
error
);
})
);
})
);
removeAttachment = effect(
exhaustMap((id: string) => {
return fromPromise(signal =>
this.store.removeEmbeddingFile(
this.workspaceService.workspace.id,
id,
signal
)
).pipe(
concatMap(() => {
this.getAttachments({ first: COUNT_PER_PAGE, after: null });
return EMPTY;
}),
catchErrorInto(this.error$, error => {
logger.error(
'Failed to remove workspace doc embedding attachment',
error
);
})
);
})
);
override dispose(): void {
this.getEnabled.unsubscribe();
this.getAttachments.unsubscribe();
this.getIgnoredDocs.unsubscribe();
this.updateIgnoredDocs.unsubscribe();
this.addAttachments.unsubscribe();
this.removeAttachment.unsubscribe();
this.setEnabled.unsubscribe();
}
}

View File

@@ -0,0 +1,20 @@
import { WorkspaceServerService } from '@affine/core/modules/cloud';
import {
WorkspaceScope,
WorkspaceService,
} from '@affine/core/modules/workspace';
import { type Framework } from '@toeverything/infra';
import { Embedding } from './entities/embedding';
import { EmbeddingService } from './services/embedding';
import { EmbeddingStore } from './stores/embedding';
export function configureIndexerEmbeddingModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(EmbeddingService)
.store(EmbeddingStore, [WorkspaceServerService])
.entity(Embedding, [WorkspaceService, EmbeddingStore]);
}
export { IndexerEmbeddingSettings } from './view';

View File

@@ -0,0 +1,7 @@
import { Service } from '@toeverything/infra';
import { Embedding } from '../entities/embedding';
export class EmbeddingService extends Service {
embedding = this.framework.createEntity(Embedding);
}

View File

@@ -0,0 +1,178 @@
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
import {
addWorkspaceEmbeddingFilesMutation,
addWorkspaceEmbeddingIgnoredDocsMutation,
getAllWorkspaceEmbeddingIgnoredDocsQuery,
getWorkspaceConfigQuery,
getWorkspaceEmbeddingFilesQuery,
type PaginationInput,
removeWorkspaceEmbeddingFilesMutation,
removeWorkspaceEmbeddingIgnoredDocsMutation,
setEnableDocEmbeddingMutation,
} from '@affine/graphql';
import { Store } from '@toeverything/infra';
export class EmbeddingStore extends Store {
constructor(private readonly workspaceServerService: WorkspaceServerService) {
super();
}
async getEnabled(workspaceId: string, signal?: AbortSignal) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getWorkspaceConfigQuery,
variables: {
id: workspaceId,
},
context: {
signal,
},
});
return data.workspace.enableDocEmbedding;
}
async updateEnabled(
workspaceId: string,
enabled: boolean,
signal?: AbortSignal
) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await this.workspaceServerService.server.gql({
query: setEnableDocEmbeddingMutation,
variables: {
id: workspaceId,
enableDocEmbedding: enabled,
},
context: {
signal,
},
});
}
async getIgnoredDocs(workspaceId: string, signal?: AbortSignal) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getAllWorkspaceEmbeddingIgnoredDocsQuery,
variables: {
workspaceId,
},
context: { signal },
});
return data.workspace.embedding.allIgnoredDocs;
}
async updateIgnoredDocs(
workspaceId: string,
add: string[],
remove: string[],
signal?: AbortSignal
) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await Promise.all([
this.workspaceServerService.server.gql({
query: addWorkspaceEmbeddingIgnoredDocsMutation,
variables: {
workspaceId,
add,
},
context: { signal },
}),
this.workspaceServerService.server.gql({
query: removeWorkspaceEmbeddingIgnoredDocsMutation,
variables: {
workspaceId,
remove,
},
context: { signal },
}),
]);
}
async addEmbeddingFile(
workspaceId: string,
blob: File,
signal?: AbortSignal
) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await this.workspaceServerService.server.gql({
query: addWorkspaceEmbeddingFilesMutation,
variables: {
workspaceId,
blob,
},
context: { signal },
});
}
async addEmbeddingFiles(
workspaceId: string,
files: File[],
signal?: AbortSignal
) {
for (const file of files) {
await this.addEmbeddingFile(workspaceId, file, signal);
}
}
async removeEmbeddingFile(
workspaceId: string,
fileId: string,
signal?: AbortSignal
) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await this.workspaceServerService.server.gql({
query: removeWorkspaceEmbeddingFilesMutation,
variables: {
workspaceId,
fileId,
},
context: { signal },
});
}
async removeEmbeddingFiles(
workspaceId: string,
fileIds: string[],
signal?: AbortSignal
) {
for (const fileId of fileIds) {
await this.removeEmbeddingFile(workspaceId, fileId, signal);
}
}
async getEmbeddingFiles(
workspaceId: string,
pagination: PaginationInput,
signal?: AbortSignal
) {
if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getWorkspaceEmbeddingFilesQuery,
variables: {
workspaceId,
pagination,
},
context: { signal },
});
return data.workspace.embedding.files;
}
}

View File

@@ -0,0 +1,12 @@
export interface AttachmentFile {
fileId: string;
fileName: string;
mimeType: string;
size: number;
createdAt: string;
}
export interface IgnoredDoc {
docId: string;
createdAt: string;
}

View File

@@ -0,0 +1,116 @@
import { Loading, useConfirmModal } from '@affine/component';
import { Pagination } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { getAttachmentFileIconRC } from '@blocksuite/affine/components/icons';
import { cssVarV2 } from '@blocksuite/affine/shared/theme';
import { CloseIcon } from '@blocksuite/icons/rc';
import { useCallback } from 'react';
import { COUNT_PER_PAGE } from '../constants';
import type { AttachmentFile } from '../types';
import {
attachmentItem,
attachmentOperation,
attachmentsWrapper,
attachmentTitle,
} from './styles-css';
interface AttachmentsProps {
attachments: AttachmentFile[];
totalCount: number;
isLoading: boolean;
onPageChange: (offset: number) => void;
onDelete: (id: string) => void;
}
interface AttachmentItemProps {
attachment: AttachmentFile;
onDelete: (id: string) => void;
}
const AttachmentItem: React.FC<AttachmentItemProps> = ({
attachment,
onDelete,
}) => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
const handleDelete = useCallback(() => {
openConfirmModal({
title:
t[
'com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.remove-attachment.title'
](),
description:
t[
'com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.remove-attachment.description'
](),
confirmText: t['Confirm'](),
confirmButtonOptions: {
variant: 'error',
},
onConfirm: () => {
onDelete(attachment.fileId);
},
});
}, [onDelete, attachment.fileId, openConfirmModal, t]);
const Icon = getAttachmentFileIconRC(attachment.mimeType);
return (
<div
className={attachmentItem}
data-testid="workspace-embedding-setting-attachment-item"
>
<div className={attachmentTitle}>
<Icon style={{ marginRight: 4 }} /> {attachment.fileName}
</div>
<div className={attachmentOperation}>
<CloseIcon
data-testid="workspace-embedding-setting-attachment-delete-button"
onClick={handleDelete}
color={cssVarV2('icon/primary')}
style={{ cursor: 'pointer' }}
/>
</div>
</div>
);
};
export const Attachments: React.FC<AttachmentsProps> = ({
attachments,
totalCount,
isLoading,
onDelete,
onPageChange,
}) => {
const handlePageChange = useCallback(
(offset: number) => {
onPageChange(offset);
},
[onPageChange]
);
return (
<div
className={attachmentsWrapper}
data-testid="workspace-embedding-setting-attachment-list"
>
{isLoading ? (
<Loading />
) : (
attachments.map(attachment => (
<AttachmentItem
key={attachment.fileId}
attachment={attachment}
onDelete={onDelete}
/>
))
)}
<Pagination
totalCount={totalCount}
countPerPage={COUNT_PER_PAGE}
onPageChange={handlePageChange}
/>
</div>
);
};

View File

@@ -0,0 +1,179 @@
import { Button, Switch } from '@affine/component';
import {
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import { Upload } from '@affine/core/components/pure/file-upload';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import type React from 'react';
import { useCallback, useMemo } from 'react';
import { EmbeddingService } from '../services/embedding';
import { Attachments } from './attachments';
import { IgnoredDocs } from './ignored-docs';
interface EmbeddingSettingsProps {}
export const EmbeddingSettings: React.FC<EmbeddingSettingsProps> = () => {
const t = useI18n();
const embeddingService = useService(EmbeddingService);
const embeddingEnabled = useLiveData(embeddingService.embedding.enabled$);
const attachments = useLiveData(embeddingService.embedding.attachments$);
const ignoredDocs = useLiveData(embeddingService.embedding.ignoredDocs$);
const isIgnoredDocsLoading = useLiveData(
embeddingService.embedding.isIgnoredDocsLoading$
);
const isAttachmentsLoading = useLiveData(
embeddingService.embedding.isAttachmentsLoading$
);
const attachmentNodes = useMemo(
() => attachments.edges.map(edge => edge.node),
[attachments]
);
const ignoredDocNodes = ignoredDocs;
const workspaceDialogService = useService(WorkspaceDialogService);
const handleEmbeddingToggle = useCallback(
(checked: boolean) => {
embeddingService.embedding.setEnabled(checked);
},
[embeddingService.embedding]
);
const handleAttachmentUpload = useCallback(
(file: File) => {
embeddingService.embedding.addAttachments([file]);
},
[embeddingService.embedding]
);
const handleAttachmentsDelete = useCallback(
(fileId: string) => {
embeddingService.embedding.removeAttachment(fileId);
},
[embeddingService.embedding]
);
const handleAttachmentsPageChange = useCallback(
(offset: number) => {
embeddingService.embedding.getAttachments({
offset,
after: attachments.pageInfo.endCursor,
});
},
[embeddingService.embedding, attachments.pageInfo.endCursor]
);
const handleSelectDoc = useCallback(() => {
if (isIgnoredDocsLoading) {
return;
}
const initialIds = ignoredDocNodes.map(doc => doc.docId);
workspaceDialogService.open(
'doc-selector',
{
init: initialIds,
},
selectedIds => {
if (selectedIds === undefined) {
return;
}
const add = selectedIds.filter(id => !initialIds?.includes(id));
const remove = initialIds?.filter(id => !selectedIds.includes(id));
embeddingService.embedding.updateIgnoredDocs({ add, remove });
}
);
}, [
ignoredDocNodes,
isIgnoredDocsLoading,
workspaceDialogService,
embeddingService.embedding,
]);
return (
<SettingWrapper
title={t[
'com.affine.settings.workspace.indexer-embedding.embedding.title'
]()}
testId="workspace-embedding-setting-wrapper"
>
<SettingRow
name=""
desc={t[
'com.affine.settings.workspace.indexer-embedding.embedding.description'
]()}
></SettingRow>
<SettingRow
name={t[
'com.affine.settings.workspace.indexer-embedding.embedding.switch.title'
]()}
desc={t[
'com.affine.settings.workspace.indexer-embedding.embedding.switch.description'
]()}
>
<Switch
data-testid="workspace-embedding-setting-switch"
checked={embeddingEnabled}
onChange={handleEmbeddingToggle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.title'
]()}
desc={t[
'com.affine.settings.workspace.indexer-embedding.embedding.additional-attachments.description'
]()}
>
<Upload fileChange={handleAttachmentUpload}>
<Button
data-testid="workspace-embedding-setting-upload-button"
variant="primary"
>
{t['Upload']()}
</Button>
</Upload>
</SettingRow>
{attachmentNodes.length > 0 && (
<Attachments
attachments={attachmentNodes}
isLoading={isAttachmentsLoading}
onDelete={handleAttachmentsDelete}
totalCount={attachments.totalCount}
onPageChange={handleAttachmentsPageChange}
/>
)}
<SettingRow
name={t[
'com.affine.settings.workspace.indexer-embedding.embedding.ignore-docs.title'
]()}
desc={t[
'com.affine.settings.workspace.indexer-embedding.embedding.ignore-docs.description'
]()}
>
<Button
data-testid="workspace-embedding-setting-ignore-docs-button"
variant="primary"
onClick={handleSelectDoc}
>
{t[
'com.affine.settings.workspace.indexer-embedding.embedding.select-doc'
]()}
</Button>
</SettingRow>
{ignoredDocNodes.length > 0 && (
<IgnoredDocs
ignoredDocs={ignoredDocNodes}
isLoading={isIgnoredDocsLoading}
/>
)}
</SettingWrapper>
);
};

View File

@@ -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<DocItemProps> = ({ 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 (
<div
className={docItem}
data-testid="workspace-embedding-setting-ignore-docs-list-item"
>
<div className={docItemTitle}>
<DocIcon className={docItemIcon} />
<span data-testid="ignore-doc-title">{docTitle}</span>
</div>
<div className={docItemInfo}>
<span>{updateDate}</span>
<span>{createDate}</span>
</div>
{/* <div className={docItemInfo}>
<span>{doc.createdAt}</span>
<Avatar name={doc.userName} url={doc.userAvatar} />
</div> */}
</div>
);
};
export const IgnoredDocs: React.FC<IgnoredDocsProps> = ({
ignoredDocs,
isLoading,
}) => {
return (
<div
className={excludeDocsWrapper}
data-testid="workspace-embedding-setting-ignore-docs-list"
>
{isLoading ? (
<Loading />
) : (
ignoredDocs.map(doc => <DocItem key={doc.docId} doc={doc} />)
)}
</div>
);
};

View File

@@ -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 (
<>
<SettingHeader
title={t['com.affine.settings.workspace.indexer-embedding.title']()}
subtitle={t[
'com.affine.settings.workspace.indexer-embedding.description'
]()}
/>
<EmbeddingSettings />
</>
);
};

View File

@@ -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',
});