diff --git a/packages/frontend/core/src/blocksuite/ai/actions/types.ts b/packages/frontend/core/src/blocksuite/ai/actions/types.ts index c4b5908be6..40e701c427 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/types.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/types.ts @@ -13,6 +13,7 @@ import type { EditorHost } from '@blocksuite/affine/std'; import type { GfxModel } from '@blocksuite/affine/std/gfx'; import type { BlockModel } from '@blocksuite/affine/store'; +import type { AIEmbeddingStatus } from '../provider'; import type { PromptKey } from '../provider/prompt'; export const translateLangs = [ @@ -422,5 +423,9 @@ declare global { query: string; }): Promise; } + + interface AIEmbeddingService { + getEmbeddingStatus(workspaceId: string): Promise; + } } } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts index c678b51618..b8d106a19c 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts @@ -39,14 +39,19 @@ export class AIChatComposer extends SignalWatcher( static override styles = css` .chat-panel-footer { margin: 8px 0px; - height: 20px; display: flex; + flex-direction: column; gap: 4px; - align-items: center; color: var(--affine-text-secondary-color); font-size: 12px; user-select: none; } + + .ai-misleading-info { + display: flex; + align-items: center; + gap: 4px; + } `; @property({ attribute: false }) @@ -148,8 +153,8 @@ export class AIChatComposer extends SignalWatcher( .addImages=${this.addImages} > `; } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/embedding-status-tooltip.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/embedding-status-tooltip.ts new file mode 100644 index 0000000000..1e992e3d76 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/embedding-status-tooltip.ts @@ -0,0 +1,95 @@ +import { SignalWatcher } from '@blocksuite/affine/global/lit'; +import { unsafeCSSVar } from '@blocksuite/affine/shared/theme'; +import type { EditorHost } from '@blocksuite/affine/std'; +import { InformationIcon } from '@blocksuite/icons/lit'; +import { css, html, LitElement } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { debounce, noop } from 'lodash-es'; + +import { AIProvider } from '../../provider/ai-provider'; + +export class AIChatEmbeddingStatusTooltip extends SignalWatcher(LitElement) { + static override styles = css` + .embedding-status { + display: flex; + align-items: center; + gap: 4px; + user-select: none; + } + .check-status { + padding: 4px; + cursor: pointer; + border-radius: 4px; + } + .check-status:hover { + background-color: ${unsafeCSSVar('--affine-hover-color')}; + } + `; + + @property({ attribute: false }) + accessor host!: EditorHost; + + @state() + accessor progressText = 'Loading embedding status...'; + + override connectedCallback() { + super.connectedCallback(); + this._updateEmbeddingStatus().catch(noop); + } + + private async _updateEmbeddingStatus() { + try { + const status = await AIProvider.embedding?.getEmbeddingStatus( + this.host.std.workspace.id + ); + if (!status) { + this.progressText = 'Loading embedding status...'; + return; + } + const completed = status.embedded === status.total; + if (completed) { + this.progressText = + 'Embedding finished. You are getting the best results!'; + } else { + this.progressText = + 'File not embedded yet. Results will improve after embedding.'; + } + this.requestUpdate(); + } catch { + this.progressText = 'Failed to load embedding status...'; + } + } + + private readonly _handleCheckStatusMouseEnter = debounce( + () => { + this._updateEmbeddingStatus().catch(noop); + }, + 1000, + { leading: true } + ); + + override render() { + return html` +
+ ${InformationIcon()} Better results after embedding finished. + + Check status + + ${this.progressText} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'ai-chat-embedding-status-tooltip': AIChatEmbeddingStatusTooltip; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/effects.ts b/packages/frontend/core/src/blocksuite/ai/effects.ts index bf95f9b87b..0f81ba675e 100644 --- a/packages/frontend/core/src/blocksuite/ai/effects.ts +++ b/packages/frontend/core/src/blocksuite/ai/effects.ts @@ -38,6 +38,7 @@ import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip'; import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip'; import { AIChatComposer } from './components/ai-chat-composer'; import { AIChatInput } from './components/ai-chat-input'; +import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip'; import { AIChatModels } from './components/ai-chat-models/ai-chat-models'; import { AIHistoryClear } from './components/ai-history-clear'; import { effects as componentAiItemEffects } from './components/ai-item'; @@ -98,6 +99,10 @@ export function registerAIEffects() { customElements.define('chat-panel-messages', ChatPanelMessages); customElements.define('chat-panel', ChatPanel); customElements.define('ai-chat-input', AIChatInput); + customElements.define( + 'ai-chat-embedding-status-tooltip', + AIChatEmbeddingStatusTooltip + ); customElements.define('ai-chat-composer', AIChatComposer); customElements.define('chat-panel-chips', ChatPanelChips); customElements.define('ai-history-clear', AIHistoryClear); diff --git a/packages/frontend/core/src/blocksuite/ai/provider/ai-provider.ts b/packages/frontend/core/src/blocksuite/ai/provider/ai-provider.ts index 53e5acb624..93c98d0e16 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/ai-provider.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/ai-provider.ts @@ -29,6 +29,11 @@ export interface AISendParams { context?: Partial; } +export interface AIEmbeddingStatus { + embedded: number; + total: number; +} + export type ActionEventType = | 'started' | 'finished' @@ -96,6 +101,10 @@ export class AIProvider { return AIProvider.instance.forkChat; } + static get embedding() { + return AIProvider.instance.embedding; + } + private static readonly instance = new AIProvider(); static LAST_ACTION_SESSIONID = ''; @@ -152,6 +161,8 @@ export class AIProvider { private userInfoFn: () => AIUserInfo | Promise | null = () => null; + private embedding: BlockSuitePresets.AIEmbeddingService | null = null; + private provideAction( id: T, action: ( @@ -307,6 +318,11 @@ export class AIProvider { static provide(id: 'onboarding', fn: (value: boolean) => void): void; + static provide( + id: 'embedding', + service: BlockSuitePresets.AIEmbeddingService + ): void; + // actions: static provide( id: T, @@ -338,6 +354,9 @@ export class AIProvider { AIProvider.instance.forkChat = action as ( options: BlockSuitePresets.AIForkChatSessionOptions ) => string | Promise; + } else if (id === 'embedding') { + AIProvider.instance.embedding = + action as BlockSuitePresets.AIEmbeddingService; } else { AIProvider.instance.provideAction(id as any, action as any); } diff --git a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts index 61f7798183..7583a0f8f4 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts @@ -13,6 +13,7 @@ import { getCopilotHistoryIdsQuery, getCopilotSessionQuery, getCopilotSessionsQuery, + getWorkspaceEmbeddingStatusQuery, type GraphQLQuery, listContextObjectQuery, listContextQuery, @@ -460,4 +461,11 @@ export class CopilotClient { }); return queryString.toString(); } + + getEmbeddingStatus(workspaceId: string) { + return this.gql({ + query: getWorkspaceEmbeddingStatusQuery, + variables: { workspaceId }, + }).then(res => res.queryWorkspaceEmbeddingStatus); + } } diff --git a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx index ea247219c7..f2ea6ef128 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx +++ b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx @@ -796,6 +796,12 @@ Could you make a new website based on these notes and send back just the html fi return client.forkSession(options); }); + AIProvider.provide('embedding', { + getEmbeddingStatus: (workspaceId: string) => { + return client.getEmbeddingStatus(workspaceId); + }, + }); + const disposeRequestLoginHandler = AIProvider.slots.requestLogin.subscribe( () => { globalDialogService.open('sign-in', {}); diff --git a/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts b/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts index c25a9d9d90..ef97482f58 100644 --- a/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts +++ b/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts @@ -17,6 +17,20 @@ test.describe('AIBasic/Chat', () => { await expect(page.getByTestId('ai-onboarding')).toBeVisible(); }); + test('should display embedding status tooltip', async ({ + loggedInPage: page, + }) => { + const check = await page.getByTestId( + 'ai-chat-embedding-status-tooltip-check' + ); + await expect(check).toBeVisible(); + + await check.hover(); + const tooltip = await page.getByTestId('ai-chat-embedding-status-tooltip'); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText(/Results will improve after embedding/i); + }); + test(`should send message and receive AI response: - send message - AI is loading