diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index 8f93bfeb19..00551a90ee 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -109,6 +109,7 @@ "@types/semver": "^7", "@vanilla-extract/css": "^1.17.0", "fake-indexeddb": "^6.0.0", + "happy-dom": "^20.3.0", "lodash-es": "^4.17.21", "vitest": "^3.2.4" } diff --git a/packages/frontend/core/src/__tests__/ai/effects.spec.ts b/packages/frontend/core/src/__tests__/ai/effects.spec.ts new file mode 100644 index 0000000000..17cce83dc1 --- /dev/null +++ b/packages/frontend/core/src/__tests__/ai/effects.spec.ts @@ -0,0 +1,30 @@ +import { + appEffectElementTags, + editorEffectElementTags, + sharedEffectElementTags, +} from '@affine/core/blocksuite/ai/effects/registry'; +import { describe, expect, test } from 'vitest'; + +describe('ai effects registration split', () => { + const editorTags = new Set([ + ...editorEffectElementTags, + ...sharedEffectElementTags, + ]); + const appTags = new Set([ + ...appEffectElementTags, + ...sharedEffectElementTags, + ]); + + test('registerAIEditorEffects skips app-only elements', () => { + expect(editorTags.has('affine-ai-chat')).toBe(true); + expect(editorTags.has('chat-panel')).toBe(false); + expect(editorTags.has('text-renderer')).toBe(true); + }); + + test('registerAIAppEffects skips editor-only elements', () => { + expect(appTags.has('ai-chat-content')).toBe(true); + expect(appTags.has('chat-panel')).toBe(false); + expect(appTags.has('affine-ai-chat')).toBe(false); + expect(appTags.has('text-renderer')).toBe(true); + }); +}); diff --git a/packages/frontend/core/src/__tests__/ai/utils/apply-model/apply-patch-to-doc.spec.ts b/packages/frontend/core/src/__tests__/ai/utils/apply-model/apply-patch-to-doc.spec.ts index 494a4f668a..b8301bc3bf 100644 --- a/packages/frontend/core/src/__tests__/ai/utils/apply-model/apply-patch-to-doc.spec.ts +++ b/packages/frontend/core/src/__tests__/ai/utils/apply-model/apply-patch-to-doc.spec.ts @@ -1,14 +1,23 @@ /** * @vitest-environment happy-dom */ +import '@blocksuite/affine-shared/test-utils'; + import { getInternalStoreExtensions } from '@blocksuite/affine/extensions/store'; import { StoreExtensionManager } from '@blocksuite/affine-ext-loader'; import { createAffineTemplate } from '@blocksuite/affine-shared/test-utils'; +import type { Store } from '@blocksuite/store'; import { describe, expect, it } from 'vitest'; import { applyPatchToDoc } from '../../../../blocksuite/ai/utils/apply-model/apply-patch-to-doc'; import type { PatchOp } from '../../../../blocksuite/ai/utils/apply-model/markdown-diff'; +declare module 'vitest' { + interface Assertion { + toEqualDoc(expected: Store, options?: { compareId?: boolean }): T; + } +} + const manager = new StoreExtensionManager(getInternalStoreExtensions()); const { affine } = createAffineTemplate(manager.get('store')); diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts index 1e8af8c957..a9d9033792 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/action-wrapper.ts @@ -26,7 +26,7 @@ import { property, state } from 'lit/decorators.js'; import { type ChatAction } from '../../components/ai-chat-messages'; import { createTextRenderer } from '../../components/text-renderer'; -import { HISTORY_IMAGE_ACTIONS } from '../const'; +import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions'; const icons: Record> = { 'Fix spelling for it': DoneIcon(), diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts deleted file mode 100644 index 2baad8c4e6..0000000000 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts +++ /dev/null @@ -1,195 +0,0 @@ -import type { AIToolsConfigService } from '@affine/core/modules/ai-button'; -import type { ServerService } from '@affine/core/modules/cloud'; -import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; -import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; -import type { AppThemeService } from '@affine/core/modules/theme'; -import type { CopilotChatHistoryFragment } from '@affine/graphql'; -import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; -import { type NotificationService } from '@blocksuite/affine/shared/services'; -import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; -import type { EditorHost } from '@blocksuite/affine/std'; -import { ShadowlessElement } from '@blocksuite/affine/std'; -import type { ExtensionType, Store } from '@blocksuite/affine/store'; -import { CenterPeekIcon } from '@blocksuite/icons/lit'; -import { css, html, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; - -import type { SearchMenuConfig } from '../components/ai-chat-add-context'; -import type { DocDisplayConfig } from '../components/ai-chat-chips'; -import type { - AIPlaygroundConfig, - AIReasoningConfig, -} from '../components/ai-chat-input'; -import type { ChatStatus } from '../components/ai-chat-messages'; -import { createPlaygroundModal } from '../components/playground/modal'; -import type { AppSidebarConfig } from './chat-config'; - -export class AIChatPanelTitle extends SignalWatcher( - WithDisposable(ShadowlessElement) -) { - static override styles = css` - .ai-chat-panel-title { - background: var(--affine-background-primary-color); - position: relative; - padding: 8px var(--h-padding, 16px); - width: 100%; - height: 36px; - display: flex; - justify-content: space-between; - align-items: center; - z-index: 1; - - svg { - width: 18px; - height: 18px; - color: var(--affine-text-secondary-color); - } - - .chat-panel-title-text { - font-size: 14px; - font-weight: 500; - color: var(--affine-text-secondary-color); - } - - .chat-panel-playground { - cursor: pointer; - padding: 2px; - margin-left: 8px; - margin-right: auto; - display: flex; - justify-content: center; - align-items: center; - } - - .chat-panel-playground:hover svg { - color: ${unsafeCSSVarV2('icon/activated')}; - } - } - `; - - @property({ attribute: false }) - accessor host!: EditorHost; - - @property({ attribute: false }) - accessor doc!: Store; - - @property({ attribute: false }) - accessor playgroundConfig!: AIPlaygroundConfig; - - @property({ attribute: false }) - accessor appSidebarConfig!: AppSidebarConfig; - - @property({ attribute: false }) - accessor reasoningConfig!: AIReasoningConfig; - - @property({ attribute: false }) - accessor searchMenuConfig!: SearchMenuConfig; - - @property({ attribute: false }) - accessor docDisplayConfig!: DocDisplayConfig; - - @property({ attribute: false }) - accessor extensions!: ExtensionType[]; - - @property({ attribute: false }) - accessor serverService!: ServerService; - - @property({ attribute: false }) - accessor affineFeatureFlagService!: FeatureFlagService; - - @property({ attribute: false }) - accessor affineWorkspaceDialogService!: WorkspaceDialogService; - - @property({ attribute: false }) - accessor affineThemeService!: AppThemeService; - - @property({ attribute: false }) - accessor notificationService!: NotificationService; - - @property({ attribute: false }) - accessor aiToolsConfigService!: AIToolsConfigService; - - @property({ attribute: false }) - accessor session!: CopilotChatHistoryFragment | null | undefined; - - @property({ attribute: false }) - accessor status!: ChatStatus; - - @property({ attribute: false }) - accessor embeddingProgress: [number, number] = [0, 0]; - - @property({ attribute: false }) - accessor newSession!: () => void; - - @property({ attribute: false }) - accessor togglePin!: () => void; - - @property({ attribute: false }) - accessor openSession!: (sessionId: string) => void; - - @property({ attribute: false }) - accessor openDoc!: (docId: string, sessionId: string) => void; - - @property({ attribute: false }) - accessor deleteSession!: (session: BlockSuitePresets.AIRecentSession) => void; - - private readonly openPlayground = () => { - const playgroundContent = html` - - `; - - createPlaygroundModal(playgroundContent, 'AI Playground'); - }; - - override render() { - const [done, total] = this.embeddingProgress; - const isEmbedding = total > 0 && done < total; - - return html` -
-
- ${isEmbedding - ? html`Embedding ${done}/${total}` - : 'AFFiNE AI'} -
- ${this.playgroundConfig.visible.value - ? html` -
- ${CenterPeekIcon()} -
- ` - : nothing} - -
- `; - } -} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts deleted file mode 100644 index beaf6c9b56..0000000000 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ /dev/null @@ -1,485 +0,0 @@ -import type { - AIDraftService, - AIToolsConfigService, -} from '@affine/core/modules/ai-button'; -import type { AIModelService } from '@affine/core/modules/ai-button/services/models'; -import type { - ServerService, - SubscriptionService, -} from '@affine/core/modules/cloud'; -import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; -import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; -import type { PeekViewService } from '@affine/core/modules/peek-view'; -import type { AppThemeService } from '@affine/core/modules/theme'; -import type { WorkbenchService } from '@affine/core/modules/workbench'; -import type { - ContextEmbedStatus, - CopilotChatHistoryFragment, - UpdateChatSessionInput, -} from '@affine/graphql'; -import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; -import { type NotificationService } from '@blocksuite/affine/shared/services'; -import type { EditorHost } from '@blocksuite/affine/std'; -import { ShadowlessElement } from '@blocksuite/affine/std'; -import type { ExtensionType, Store } from '@blocksuite/affine/store'; -import { type Signal, signal } from '@preact/signals-core'; -import { css, html, type PropertyValues } from 'lit'; -import { property, state } from 'lit/decorators.js'; -import { keyed } from 'lit/directives/keyed.js'; - -import { AffineIcon } from '../_common/icons'; -import type { SearchMenuConfig } from '../components/ai-chat-add-context'; -import type { DocDisplayConfig } from '../components/ai-chat-chips'; -import type { ChatContextValue } from '../components/ai-chat-content'; -import type { - AIPlaygroundConfig, - AIReasoningConfig, -} from '../components/ai-chat-input'; -import type { ChatStatus } from '../components/ai-chat-messages'; -import { AIProvider } from '../provider'; -import type { AppSidebarConfig } from './chat-config'; - -export class ChatPanel extends SignalWatcher( - WithDisposable(ShadowlessElement) -) { - static override styles = css` - chat-panel { - width: 100%; - user-select: text; - - .chat-panel-container { - height: 100%; - display: flex; - flex-direction: column; - } - - ai-chat-content { - height: 0; - flex-grow: 1; - } - - .chat-loading-container { - position: relative; - padding: 44px 0 166px 0; - height: 100%; - display: flex; - align-items: center; - } - - .chat-loading { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - } - - .chat-loading-title { - font-weight: 600; - font-size: var(--affine-font-sm); - color: var(--affine-text-secondary-color); - } - } - `; - - @property({ attribute: false }) - accessor host!: EditorHost; - - @property({ attribute: false }) - accessor doc!: Store; - - @property({ attribute: false }) - accessor playgroundConfig!: AIPlaygroundConfig; - - @property({ attribute: false }) - accessor appSidebarConfig!: AppSidebarConfig; - - @property({ attribute: false }) - accessor reasoningConfig!: AIReasoningConfig; - - @property({ attribute: false }) - accessor searchMenuConfig!: SearchMenuConfig; - - @property({ attribute: false }) - accessor docDisplayConfig!: DocDisplayConfig; - - @property({ attribute: false }) - accessor extensions!: ExtensionType[]; - - @property({ attribute: false }) - accessor serverService!: ServerService; - - @property({ attribute: false }) - accessor affineFeatureFlagService!: FeatureFlagService; - - @property({ attribute: false }) - accessor affineWorkspaceDialogService!: WorkspaceDialogService; - - @property({ attribute: false }) - accessor affineWorkbenchService!: WorkbenchService; - - @property({ attribute: false }) - accessor affineThemeService!: AppThemeService; - - @property({ attribute: false }) - accessor notificationService!: NotificationService; - - @property({ attribute: false }) - accessor aiDraftService!: AIDraftService; - - @property({ attribute: false }) - accessor aiToolsConfigService!: AIToolsConfigService; - - @property({ attribute: false }) - accessor peekViewService!: PeekViewService; - - @property({ attribute: false }) - accessor subscriptionService!: SubscriptionService; - - @property({ attribute: false }) - accessor aiModelService!: AIModelService; - - @property({ attribute: false }) - accessor onAISubscribe!: () => Promise; - - @state() - accessor session: CopilotChatHistoryFragment | null | undefined; - - @state() - accessor embeddingProgress: [number, number] = [0, 0]; - - @state() - accessor status: ChatStatus = 'idle'; - - private isSidebarOpen: Signal = signal(false); - - private sidebarWidth: Signal = signal(undefined); - - private hasPinned = false; - - private get isInitialized() { - return this.session !== undefined; - } - - private readonly getSessionIdFromUrl = () => { - if (this.affineWorkbenchService) { - const { workbench } = this.affineWorkbenchService; - const location = workbench.location$.value; - const searchParams = new URLSearchParams(location.search); - const sessionId = searchParams.get('sessionId'); - if (sessionId) { - workbench.activeView$.value.updateQueryString( - { sessionId: undefined }, - { replace: true } - ); - } - return sessionId; - } - return undefined; - }; - - private readonly setSession = ( - session: CopilotChatHistoryFragment | null | undefined - ) => { - this.session = session ?? null; - }; - - private readonly initSession = async () => { - if (!AIProvider.session) { - return; - } - const sessionId = this.getSessionIdFromUrl(); - const pinSessions = await AIProvider.session.getSessions( - this.doc.workspace.id, - undefined, - { pinned: true, limit: 1 } - ); - - if (Array.isArray(pinSessions) && pinSessions[0]) { - // pinned session - this.session = pinSessions[0]; - } else if (sessionId) { - // sessionId from url - const session = await AIProvider.session.getSession( - this.doc.workspace.id, - sessionId - ); - this.setSession(session); - } else { - // latest doc session - const docSessions = await AIProvider.session.getSessions( - this.doc.workspace.id, - this.doc.id, - { action: false, fork: false, limit: 1 } - ); - // sessions is descending ordered by updatedAt - // the first item is the latest session - const session = docSessions?.[0]; - this.setSession(session); - } - }; - - private readonly createSession = async ( - options: Partial = {} - ) => { - if (this.session) { - return this.session; - } - const sessionId = await AIProvider.session?.createSession({ - docId: this.doc.id, - workspaceId: this.doc.workspace.id, - promptName: 'Chat With AFFiNE AI', - reuseLatestChat: false, - ...options, - }); - if (sessionId) { - const session = await AIProvider.session?.getSession( - this.doc.workspace.id, - sessionId - ); - this.setSession(session); - } - return this.session; - }; - - private readonly deleteSession = async ( - session: BlockSuitePresets.AIRecentSession - ) => { - if (!AIProvider.histories) { - return; - } - const confirm = await this.notificationService.confirm({ - title: 'Delete this history?', - message: - 'Do you want to delete this AI conversation history? Once deleted, it cannot be recovered.', - confirmText: 'Delete', - cancelText: 'Cancel', - }); - if (confirm) { - await AIProvider.histories.cleanup( - session.workspaceId, - session.docId || undefined, - [session.sessionId] - ); - if (session.sessionId === this.session?.sessionId) { - this.newSession(); - } - } - }; - - private readonly updateSession = async (options: UpdateChatSessionInput) => { - await AIProvider.session?.updateSession(options); - const session = await AIProvider.session?.getSession( - this.doc.workspace.id, - options.sessionId - ); - this.setSession(session); - }; - - private readonly newSession = () => { - this.resetPanel(); - requestAnimationFrame(() => { - this.session = null; - }); - }; - - private readonly openSession = async (sessionId: string) => { - if (this.session?.sessionId === sessionId) { - return; - } - this.resetPanel(); - const session = await AIProvider.session?.getSession( - this.doc.workspace.id, - sessionId - ); - this.setSession(session); - }; - - private readonly openDoc = async (docId: string, sessionId: string) => { - if (this.doc.id === docId) { - if (this.session?.sessionId === sessionId || this.session?.pinned) { - return; - } - await this.openSession(sessionId); - } else if (this.affineWorkbenchService) { - const { workbench } = this.affineWorkbenchService; - if (this.session?.pinned) { - workbench.open(`/${docId}`, { at: 'active' }); - } else { - workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' }); - } - } - }; - - private readonly togglePin = async () => { - const pinned = !this.session?.pinned; - this.hasPinned = true; - if (!this.session) { - await this.createSession({ pinned }); - } else { - await this.updateSession({ - sessionId: this.session.sessionId, - pinned, - }); - } - }; - - private readonly rebindSession = async () => { - if (!this.session) { - return; - } - if (this.session.docId !== this.doc.id) { - await this.updateSession({ - sessionId: this.session.sessionId, - docId: this.doc.id, - }); - } - }; - - private readonly initPanel = async () => { - try { - if (!this.isSidebarOpen.value) { - return; - } - await this.initSession(); - this.hasPinned = !!this.session?.pinned; - } catch (error) { - console.error(error); - } - }; - - private readonly resetPanel = () => { - this.session = undefined; - this.embeddingProgress = [0, 0]; - this.hasPinned = false; - }; - - private readonly onEmbeddingProgressChange = ( - count: Record - ) => { - const total = count.finished + count.processing + count.failed; - this.embeddingProgress = [count.finished, total]; - }; - - private readonly onContextChange = async ( - context: Partial - ) => { - this.status = context.status ?? 'idle'; - if (context.status === 'success') { - await this.rebindSession(); - } - }; - - protected override updated(changedProperties: PropertyValues) { - if (changedProperties.has('doc')) { - if (this.session?.pinned) { - return; - } - this.resetPanel(); - this.initPanel().catch(console.error); - } - } - - override connectedCallback() { - super.connectedCallback(); - if (!this.doc) throw new Error('doc is required'); - - this._disposables.add( - AIProvider.slots.userInfo.subscribe(() => { - this.resetPanel(); - this.initPanel().catch(console.error); - }) - ); - - const isOpen = this.appSidebarConfig.isOpen(); - this.isSidebarOpen = isOpen.signal; - this._disposables.add(isOpen.cleanup); - - const width = this.appSidebarConfig.getWidth(); - this.sidebarWidth = width.signal; - this._disposables.add(width.cleanup); - - this._disposables.add( - this.isSidebarOpen.subscribe(isOpen => { - if (isOpen && !this.isInitialized) { - this.initPanel().catch(console.error); - } - }) - ); - } - - override render() { - if (!this.isInitialized) { - return html`
-
- ${AffineIcon('var(--affine-icon-secondary)')} -
- AFFiNE AI is loading history... -
-
-
`; - } - - return html`
- - ${keyed( - this.hasPinned ? this.session?.sessionId : this.doc.id, - html`` - )} -
`; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'chat-panel': ChatPanel; - } -} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts index 2c1b5eb5b5..3bca61c7a3 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts @@ -5,7 +5,7 @@ import { html } from 'lit'; import { property } from 'lit/decorators.js'; import { type ChatAction } from '../../components/ai-chat-messages'; -import { HISTORY_IMAGE_ACTIONS } from '../const'; +import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions'; export class ChatMessageAction extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts index 15e9feb921..1849d261c6 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts @@ -28,9 +28,9 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import { styleMap } from 'lit/directives/style-map.js'; import { pick } from 'lodash-es'; -import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const'; import { type AIChatParams, AIProvider } from '../../provider/ai-provider'; import { extractSelectedContent } from '../../utils/extract'; +import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions'; import type { SearchMenuConfig } from '../ai-chat-add-context'; import type { DocDisplayConfig } from '../ai-chat-chips'; import type { AIReasoningConfig } from '../ai-chat-input'; diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/split-view.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/split-view.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/split-view.ts rename to packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/split-view.ts diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/ai-chat-messages.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/ai-chat-messages.ts index 9b9c6e399b..28d09d1b1a 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/ai-chat-messages.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/ai-chat-messages.ts @@ -19,12 +19,12 @@ import { repeat } from 'lit/directives/repeat.js'; import { debounce } from 'lodash-es'; import { AffineIcon } from '../../_common/icons'; -import { AIPreloadConfig } from '../../chat-panel/preload-config'; import { type AIError, AIProvider, UnauthorizedError } from '../../provider'; import { mergeStreamObjects } from '../../utils/stream-objects'; import type { DocDisplayConfig } from '../ai-chat-chips'; import { type ChatContextValue } from '../ai-chat-content/type'; import type { AIReasoningConfig } from '../ai-chat-input'; +import { AIPreloadConfig } from './preload-config'; import { type HistoryMessage, isChatAction, diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/preload-config.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/preload-config.ts similarity index 96% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/preload-config.ts rename to packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/preload-config.ts index 7fd46e0ae3..fad63136f3 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/preload-config.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/preload-config.ts @@ -6,7 +6,7 @@ import { SendIcon, } from '@blocksuite/icons/lit'; -import { AIProvider } from '../provider/ai-provider.js'; +import { AIProvider } from '../../provider/ai-provider.js'; import completeWritingWithAI from './templates/completeWritingWithAI.zip'; import freelyCommunicateWithAI from './templates/freelyCommunicateWithAI.zip'; import readAforeign from './templates/readAforeign.zip'; diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/templates/TidyMindMapV3.zip b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/TidyMindMapV3.zip similarity index 100% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/templates/TidyMindMapV3.zip rename to packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/TidyMindMapV3.zip diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/templates/completeWritingWithAI.zip b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/completeWritingWithAI.zip similarity index 100% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/templates/completeWritingWithAI.zip rename to packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/completeWritingWithAI.zip diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/templates/freelyCommunicateWithAI.zip b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/freelyCommunicateWithAI.zip similarity index 100% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/templates/freelyCommunicateWithAI.zip rename to packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/freelyCommunicateWithAI.zip diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/templates/readAforeign.zip b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/readAforeign.zip similarity index 100% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/templates/readAforeign.zip rename to packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/readAforeign.zip diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/templates/redHat.zip b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/redHat.zip similarity index 100% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/templates/redHat.zip rename to packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/templates/redHat.zip diff --git a/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts b/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts index a615143b3c..15f7e8b8fe 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts @@ -1,5 +1,9 @@ import type { AIToolsConfigService } from '@affine/core/modules/ai-button'; -import type { ServerService } from '@affine/core/modules/cloud'; +import type { AIModelService } from '@affine/core/modules/ai-button/services/models'; +import type { + ServerService, + SubscriptionService, +} from '@affine/core/modules/cloud'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { AppThemeService } from '@affine/core/modules/theme'; @@ -20,8 +24,8 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import { throttle } from 'lodash-es'; import type { AppSidebarConfig } from '../../chat-panel/chat-config'; -import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const'; import { AIProvider } from '../../provider'; +import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions'; import type { SearchMenuConfig } from '../ai-chat-add-context'; import type { DocDisplayConfig } from '../ai-chat-chips'; import type { ChatContextValue } from '../ai-chat-content'; @@ -179,6 +183,12 @@ export class PlaygroundChat extends SignalWatcher( @property({ attribute: false }) accessor aiToolsConfigService!: AIToolsConfigService; + @property({ attribute: false }) + accessor subscriptionService!: SubscriptionService; + + @property({ attribute: false }) + accessor aiModelService!: AIModelService; + @property({ attribute: false }) accessor onAISubscribe: (() => Promise) | undefined; @@ -373,6 +383,8 @@ export class PlaygroundChat extends SignalWatcher( .aiToolsConfigService=${this.aiToolsConfigService} .affineWorkspaceDialogService=${this.affineWorkspaceDialogService} .affineFeatureFlagService=${this.affineFeatureFlagService} + .subscriptionService=${this.subscriptionService} + .aiModelService=${this.aiModelService} .onAISubscribe=${this.onAISubscribe} > `; diff --git a/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts b/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts index 9064ead626..f3c0204ae2 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts @@ -1,5 +1,10 @@ import type { AIToolsConfigService } from '@affine/core/modules/ai-button'; -import type { ServerService } from '@affine/core/modules/cloud'; +import type { AIModelService } from '@affine/core/modules/ai-button/services/models'; +import type { + ServerService, + SubscriptionService, +} from '@affine/core/modules/cloud'; +import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { AppThemeService } from '@affine/core/modules/theme'; import type { CopilotChatHistoryFragment } from '@affine/graphql'; @@ -93,6 +98,15 @@ export class PlaygroundContent extends SignalWatcher( @property({ attribute: false }) accessor aiToolsConfigService!: AIToolsConfigService; + @property({ attribute: false }) + accessor affineWorkspaceDialogService!: WorkspaceDialogService; + + @property({ attribute: false }) + accessor subscriptionService!: SubscriptionService; + + @property({ attribute: false }) + accessor aiModelService!: AIModelService; + @state() accessor sessions: CopilotChatHistoryFragment[] = []; @@ -351,6 +365,10 @@ export class PlaygroundContent extends SignalWatcher( .affineThemeService=${this.affineThemeService} .notificationService=${this.notificationService} .aiToolsConfigService=${this.aiToolsConfigService} + .affineWorkspaceDialogService=${this + .affineWorkspaceDialogService} + .subscriptionService=${this.subscriptionService} + .aiModelService=${this.aiModelService} .addChat=${this.addChat} > diff --git a/packages/frontend/core/src/blocksuite/ai/effects.ts b/packages/frontend/core/src/blocksuite/ai/effects.ts deleted file mode 100644 index c330480e23..0000000000 --- a/packages/frontend/core/src/blocksuite/ai/effects.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { effects as tooltipEffects } from '@blocksuite/affine-components/tooltip'; - -import { AIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-block'; -import { EdgelessAIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-edgeless-block'; -import { LitTranscriptionBlock } from './blocks/ai-chat-block/ai-transcription-block'; -import { - AIChatBlockMessage, - AIChatBlockMessages, -} from './blocks/ai-chat-block/components/ai-chat-messages'; -import { - ChatImage, - ChatImages, -} from './blocks/ai-chat-block/components/chat-images'; -import { ImagePlaceholder } from './blocks/ai-chat-block/components/image-placeholder'; -import { UserInfo } from './blocks/ai-chat-block/components/user-info'; -import { ChatPanel } from './chat-panel'; -import { ActionWrapper } from './chat-panel/actions/action-wrapper'; -import { ActionImage } from './chat-panel/actions/image'; -import { ActionImageToText } from './chat-panel/actions/image-to-text'; -import { ActionMakeReal } from './chat-panel/actions/make-real'; -import { ActionMindmap } from './chat-panel/actions/mindmap'; -import { ActionSlides } from './chat-panel/actions/slides'; -import { ActionText } from './chat-panel/actions/text'; -import { AILoading } from './chat-panel/ai-loading'; -import { AIChatPanelTitle } from './chat-panel/ai-title'; -import { ChatMessageAction } from './chat-panel/message/action'; -import { ChatMessageAssistant } from './chat-panel/message/assistant'; -import { ChatMessageUser } from './chat-panel/message/user'; -import { ChatPanelSplitView } from './chat-panel/split-view'; -import { ArtifactSkeleton } from './components/ai-artifact-skeleton'; -import { AIChatAddContext } from './components/ai-chat-add-context'; -import { ChatPanelAddPopover } from './components/ai-chat-chips/add-popover'; -import { ChatPanelAttachmentChip } from './components/ai-chat-chips/attachment-chip'; -import { ChatPanelCandidatesPopover } from './components/ai-chat-chips/candidates-popover'; -import { ChatPanelChips } from './components/ai-chat-chips/chat-panel-chips'; -import { ChatPanelChip } from './components/ai-chat-chips/chip'; -import { ChatPanelCollectionChip } from './components/ai-chat-chips/collection-chip'; -import { ChatPanelDocChip } from './components/ai-chat-chips/doc-chip'; -import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip'; -import { ChatPanelSelectedChip } from './components/ai-chat-chips/selected-chip'; -import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip'; -import { AIChatComposer } from './components/ai-chat-composer'; -import { AIChatContent } from './components/ai-chat-content'; -import { AIChatInput } from './components/ai-chat-input'; -import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip'; -import { ChatInputPreference } from './components/ai-chat-input/preference-popup'; -import { AIChatMessages } from './components/ai-chat-messages/ai-chat-messages'; -import { AIChatToolbar, AISessionHistory } from './components/ai-chat-toolbar'; -import { AIHistoryClear } from './components/ai-history-clear'; -import { effects as componentAiItemEffects } from './components/ai-item'; -import { AssistantAvatar } from './components/ai-message-content/assistant-avatar'; -import { ChatContentImages } from './components/ai-message-content/images'; -import { ChatContentPureText } from './components/ai-message-content/pure-text'; -import { ChatContentRichText } from './components/ai-message-content/rich-text'; -import { ChatContentStreamObjects } from './components/ai-message-content/stream-objects'; -import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer'; -import { ArtifactPreviewPanel } from './components/ai-tools/artifacts-preview-panel'; -import { - CodeArtifactTool, - CodeHighlighter, -} from './components/ai-tools/code-artifact'; -import { DocComposeTool } from './components/ai-tools/doc-compose'; -import { DocEditTool } from './components/ai-tools/doc-edit'; -import { DocKeywordSearchResult } from './components/ai-tools/doc-keyword-search-result'; -import { DocReadResult } from './components/ai-tools/doc-read-result'; -import { DocSemanticSearchResult } from './components/ai-tools/doc-semantic-search-result'; -import { DocWriteTool } from './components/ai-tools/doc-write'; -import { SectionEditTool } from './components/ai-tools/section-edit'; -import { ToolCallCard } from './components/ai-tools/tool-call-card'; -import { ToolFailedCard } from './components/ai-tools/tool-failed-card'; -import { ToolResultCard } from './components/ai-tools/tool-result-card'; -import { WebCrawlTool } from './components/ai-tools/web-crawl'; -import { WebSearchTool } from './components/ai-tools/web-search'; -import { AskAIButton } from './components/ask-ai-button'; -import { AskAIIcon } from './components/ask-ai-icon'; -import { AskAIPanel } from './components/ask-ai-panel'; -import { AskAIToolbarButton } from './components/ask-ai-toolbar'; -import { ChatActionList } from './components/chat-action-list'; -import { ChatCopyMore } from './components/copy-more'; -import { ImagePreviewGrid } from './components/image-preview-grid'; -import { effects as componentPlaygroundEffects } from './components/playground'; -import { TextRenderer } from './components/text-renderer'; -import { AIErrorWrapper } from './messages/error'; -import { AISlidesRenderer } from './messages/slides-renderer'; -import { AIAnswerWrapper } from './messages/wrapper'; -import { registerMiniMindmapBlocks } from './mini-mindmap'; -import { AIChatBlockPeekView } from './peek-view/chat-block-peek-view'; -import { DateTime } from './peek-view/date-time'; -import { - AFFINE_AI_PANEL_WIDGET, - AffineAIPanelWidget, -} from './widgets/ai-panel/ai-panel'; -import { - AIPanelAnswer, - AIPanelDivider, - AIPanelError, - AIPanelGenerating, - AIPanelInput, -} from './widgets/ai-panel/components'; -import { AIFinishTip } from './widgets/ai-panel/components/finish-tip'; -import { GeneratingPlaceholder } from './widgets/ai-panel/components/generating-placeholder'; -import { - AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK, - AffineBlockDiffWidgetForBlock, -} from './widgets/block-diff/block'; -import { BlockDiffOptions } from './widgets/block-diff/options'; -import { - AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE, - AffineBlockDiffWidgetForPage, -} from './widgets/block-diff/page'; -import { - AFFINE_BLOCK_DIFF_PLAYGROUND, - AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL, - BlockDiffPlayground, - BlockDiffPlaygroundModal, -} from './widgets/block-diff/playground'; -import { - AFFINE_EDGELESS_COPILOT_WIDGET, - EdgelessCopilotWidget, -} from './widgets/edgeless-copilot'; -import { EdgelessCopilotPanel } from './widgets/edgeless-copilot-panel'; -import { EdgelessCopilotToolbarEntry } from './widgets/edgeless-copilot-panel/toolbar-entry'; - -export function registerAIEffects() { - registerMiniMindmapBlocks(); - componentAiItemEffects(); - componentPlaygroundEffects(); - tooltipEffects(); - - customElements.define('ask-ai-icon', AskAIIcon); - customElements.define('ask-ai-button', AskAIButton); - customElements.define('ask-ai-toolbar-button', AskAIToolbarButton); - customElements.define('ask-ai-panel', AskAIPanel); - customElements.define('chat-action-list', ChatActionList); - customElements.define('chat-copy-more', ChatCopyMore); - customElements.define('image-preview-grid', ImagePreviewGrid); - customElements.define('action-wrapper', ActionWrapper); - customElements.define('action-image-to-text', ActionImageToText); - customElements.define('action-image', ActionImage); - customElements.define('action-make-real', ActionMakeReal); - customElements.define('action-mindmap', ActionMindmap); - customElements.define('action-slides', ActionSlides); - customElements.define('action-text', ActionText); - customElements.define('ai-loading', AILoading); - customElements.define('ai-chat-content', AIChatContent); - customElements.define('ai-chat-toolbar', AIChatToolbar); - customElements.define('ai-session-history', AISessionHistory); - customElements.define('ai-chat-messages', AIChatMessages); - customElements.define('chat-panel', ChatPanel); - customElements.define('ai-chat-panel-title', AIChatPanelTitle); - customElements.define('ai-chat-input', AIChatInput); - customElements.define('ai-chat-add-context', AIChatAddContext); - 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); - customElements.define('chat-panel-add-popover', ChatPanelAddPopover); - customElements.define('chat-input-preference', ChatInputPreference); - customElements.define( - 'chat-panel-candidates-popover', - ChatPanelCandidatesPopover - ); - customElements.define('chat-panel-doc-chip', ChatPanelDocChip); - customElements.define('chat-panel-file-chip', ChatPanelFileChip); - customElements.define('chat-panel-tag-chip', ChatPanelTagChip); - customElements.define('chat-panel-collection-chip', ChatPanelCollectionChip); - customElements.define('chat-panel-selected-chip', ChatPanelSelectedChip); - customElements.define('chat-panel-attachment-chip', ChatPanelAttachmentChip); - customElements.define('chat-panel-chip', ChatPanelChip); - customElements.define('ai-error-wrapper', AIErrorWrapper); - customElements.define('ai-slides-renderer', AISlidesRenderer); - customElements.define('ai-answer-wrapper', AIAnswerWrapper); - customElements.define('ai-chat-block-peek-view', AIChatBlockPeekView); - customElements.define('date-time', DateTime); - customElements.define( - 'affine-edgeless-ai-chat', - EdgelessAIChatBlockComponent - ); - customElements.define('affine-ai-chat', AIChatBlockComponent); - customElements.define('ai-chat-block-message', AIChatBlockMessage); - customElements.define('ai-chat-block-messages', AIChatBlockMessages); - customElements.define( - 'ai-scrollable-text-renderer', - AIScrollableTextRenderer - ); - customElements.define('image-placeholder', ImagePlaceholder); - customElements.define('chat-image', ChatImage); - customElements.define('chat-images', ChatImages); - customElements.define('user-info', UserInfo); - customElements.define('text-renderer', TextRenderer); - - customElements.define('generating-placeholder', GeneratingPlaceholder); - customElements.define('ai-finish-tip', AIFinishTip); - customElements.define('ai-panel-divider', AIPanelDivider); - customElements.define('ai-panel-answer', AIPanelAnswer); - customElements.define('ai-panel-input', AIPanelInput); - customElements.define('ai-panel-generating', AIPanelGenerating); - customElements.define('ai-panel-error', AIPanelError); - customElements.define('chat-assistant-avatar', AssistantAvatar); - customElements.define('chat-content-images', ChatContentImages); - customElements.define('chat-content-pure-text', ChatContentPureText); - customElements.define('chat-content-rich-text', ChatContentRichText); - customElements.define( - 'chat-content-stream-objects', - ChatContentStreamObjects - ); - customElements.define('chat-message-action', ChatMessageAction); - customElements.define('chat-message-assistant', ChatMessageAssistant); - customElements.define('chat-message-user', ChatMessageUser); - customElements.define('ai-block-diff-options', BlockDiffOptions); - customElements.define(AFFINE_BLOCK_DIFF_PLAYGROUND, BlockDiffPlayground); - customElements.define( - AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL, - BlockDiffPlaygroundModal - ); - - customElements.define('tool-call-card', ToolCallCard); - customElements.define('tool-result-card', ToolResultCard); - customElements.define('tool-call-failed', ToolFailedCard); - customElements.define('doc-semantic-search-result', DocSemanticSearchResult); - customElements.define('doc-keyword-search-result', DocKeywordSearchResult); - customElements.define('doc-read-result', DocReadResult); - customElements.define('doc-write-tool', DocWriteTool); - customElements.define('web-crawl-tool', WebCrawlTool); - customElements.define('web-search-tool', WebSearchTool); - customElements.define('section-edit-tool', SectionEditTool); - customElements.define('doc-compose-tool', DocComposeTool); - customElements.define('code-artifact-tool', CodeArtifactTool); - customElements.define('code-highlighter', CodeHighlighter); - customElements.define('artifact-preview-panel', ArtifactPreviewPanel); - customElements.define('doc-edit-tool', DocEditTool); - - customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget); - customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget); - customElements.define( - AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK, - AffineBlockDiffWidgetForBlock - ); - customElements.define( - AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE, - AffineBlockDiffWidgetForPage - ); - - customElements.define('edgeless-copilot-panel', EdgelessCopilotPanel); - customElements.define( - 'edgeless-copilot-toolbar-entry', - EdgelessCopilotToolbarEntry - ); - - customElements.define('transcription-block', LitTranscriptionBlock); - customElements.define('chat-panel-split-view', ChatPanelSplitView); - customElements.define('artifact-skeleton', ArtifactSkeleton); -} diff --git a/packages/frontend/core/src/blocksuite/ai/effects/app.ts b/packages/frontend/core/src/blocksuite/ai/effects/app.ts new file mode 100644 index 0000000000..dd8acbb47c --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/effects/app.ts @@ -0,0 +1,95 @@ +import { ActionWrapper } from '../chat-panel/actions/action-wrapper'; +import { ActionImage } from '../chat-panel/actions/image'; +import { ActionImageToText } from '../chat-panel/actions/image-to-text'; +import { ActionMakeReal } from '../chat-panel/actions/make-real'; +import { ActionMindmap } from '../chat-panel/actions/mindmap'; +import { ActionSlides } from '../chat-panel/actions/slides'; +import { ActionText } from '../chat-panel/actions/text'; +import { AILoading } from '../chat-panel/ai-loading'; +import { ChatMessageAction } from '../chat-panel/message/action'; +import { ChatMessageAssistant } from '../chat-panel/message/assistant'; +import { ChatMessageUser } from '../chat-panel/message/user'; +import { AIChatAddContext } from '../components/ai-chat-add-context'; +import { ChatPanelAddPopover } from '../components/ai-chat-chips/add-popover'; +import { ChatPanelAttachmentChip } from '../components/ai-chat-chips/attachment-chip'; +import { ChatPanelCandidatesPopover } from '../components/ai-chat-chips/candidates-popover'; +import { ChatPanelChips } from '../components/ai-chat-chips/chat-panel-chips'; +import { ChatPanelChip } from '../components/ai-chat-chips/chip'; +import { ChatPanelCollectionChip } from '../components/ai-chat-chips/collection-chip'; +import { ChatPanelDocChip } from '../components/ai-chat-chips/doc-chip'; +import { ChatPanelFileChip } from '../components/ai-chat-chips/file-chip'; +import { ChatPanelSelectedChip } from '../components/ai-chat-chips/selected-chip'; +import { ChatPanelTagChip } from '../components/ai-chat-chips/tag-chip'; +import { AIChatComposer } from '../components/ai-chat-composer'; +import { AIChatContent } from '../components/ai-chat-content'; +import { ChatPanelSplitView } from '../components/ai-chat-content/split-view'; +import { AIChatInput } from '../components/ai-chat-input'; +import { AIChatEmbeddingStatusTooltip } from '../components/ai-chat-input/embedding-status-tooltip'; +import { ChatInputPreference } from '../components/ai-chat-input/preference-popup'; +import { AIChatMessages } from '../components/ai-chat-messages/ai-chat-messages'; +import { AIChatToolbar, AISessionHistory } from '../components/ai-chat-toolbar'; +import { AIHistoryClear } from '../components/ai-history-clear'; +import { AssistantAvatar } from '../components/ai-message-content/assistant-avatar'; +import { ChatActionList } from '../components/chat-action-list'; +import { ChatCopyMore } from '../components/copy-more'; +import { ImagePreviewGrid } from '../components/image-preview-grid'; +import { effects as componentPlaygroundEffects } from '../components/playground'; +import { AIChatBlockPeekView } from '../peek-view/chat-block-peek-view'; +import { DateTime } from '../peek-view/date-time'; +import { type AppEffectElementTag, appEffectElementTags } from './registry'; +import { registerAISharedEffects } from './shared'; + +const appRegistries = new WeakSet(); +const appElements = { + 'chat-action-list': ChatActionList, + 'chat-copy-more': ChatCopyMore, + 'image-preview-grid': ImagePreviewGrid, + 'action-wrapper': ActionWrapper, + 'action-image-to-text': ActionImageToText, + 'action-image': ActionImage, + 'action-make-real': ActionMakeReal, + 'action-mindmap': ActionMindmap, + 'action-slides': ActionSlides, + 'action-text': ActionText, + 'ai-loading': AILoading, + 'ai-chat-content': AIChatContent, + 'ai-chat-toolbar': AIChatToolbar, + 'ai-session-history': AISessionHistory, + 'ai-chat-messages': AIChatMessages, + 'ai-chat-input': AIChatInput, + 'ai-chat-add-context': AIChatAddContext, + 'ai-chat-embedding-status-tooltip': AIChatEmbeddingStatusTooltip, + 'ai-chat-composer': AIChatComposer, + 'chat-panel-chips': ChatPanelChips, + 'ai-history-clear': AIHistoryClear, + 'chat-panel-add-popover': ChatPanelAddPopover, + 'chat-input-preference': ChatInputPreference, + 'chat-panel-candidates-popover': ChatPanelCandidatesPopover, + 'chat-panel-doc-chip': ChatPanelDocChip, + 'chat-panel-file-chip': ChatPanelFileChip, + 'chat-panel-tag-chip': ChatPanelTagChip, + 'chat-panel-collection-chip': ChatPanelCollectionChip, + 'chat-panel-selected-chip': ChatPanelSelectedChip, + 'chat-panel-attachment-chip': ChatPanelAttachmentChip, + 'chat-panel-chip': ChatPanelChip, + 'chat-assistant-avatar': AssistantAvatar, + 'chat-message-action': ChatMessageAction, + 'chat-message-assistant': ChatMessageAssistant, + 'chat-message-user': ChatMessageUser, + 'ai-chat-block-peek-view': AIChatBlockPeekView, + 'date-time': DateTime, + 'chat-panel-split-view': ChatPanelSplitView, +} satisfies Record; + +export function registerAIAppEffects() { + const registry = customElements; + if (appRegistries.has(registry)) return; + appRegistries.add(registry); + + registerAISharedEffects(); + componentPlaygroundEffects(); + + for (const tag of appEffectElementTags) { + customElements.define(tag, appElements[tag]); + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/effects/editor.ts b/packages/frontend/core/src/blocksuite/ai/effects/editor.ts new file mode 100644 index 0000000000..f66b9f87fe --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/effects/editor.ts @@ -0,0 +1,105 @@ +import { AIChatBlockComponent } from '../blocks/ai-chat-block/ai-chat-block'; +import { EdgelessAIChatBlockComponent } from '../blocks/ai-chat-block/ai-chat-edgeless-block'; +import { LitTranscriptionBlock } from '../blocks/ai-chat-block/ai-transcription-block'; +import { + AIChatBlockMessage, + AIChatBlockMessages, +} from '../blocks/ai-chat-block/components/ai-chat-messages'; +import { + ChatImage, + ChatImages, +} from '../blocks/ai-chat-block/components/chat-images'; +import { ImagePlaceholder } from '../blocks/ai-chat-block/components/image-placeholder'; +import { UserInfo } from '../blocks/ai-chat-block/components/user-info'; +import { effects as componentAiItemEffects } from '../components/ai-item'; +import { AIScrollableTextRenderer } from '../components/ai-scrollable-text-renderer'; +import { AskAIButton } from '../components/ask-ai-button'; +import { AskAIIcon } from '../components/ask-ai-icon'; +import { AskAIPanel } from '../components/ask-ai-panel'; +import { AskAIToolbarButton } from '../components/ask-ai-toolbar'; +import { + AFFINE_AI_PANEL_WIDGET, + AffineAIPanelWidget, +} from '../widgets/ai-panel/ai-panel'; +import { + AIPanelAnswer, + AIPanelDivider, + AIPanelError, + AIPanelGenerating, + AIPanelInput, +} from '../widgets/ai-panel/components'; +import { AIFinishTip } from '../widgets/ai-panel/components/finish-tip'; +import { GeneratingPlaceholder } from '../widgets/ai-panel/components/generating-placeholder'; +import { + AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK, + AffineBlockDiffWidgetForBlock, +} from '../widgets/block-diff/block'; +import { BlockDiffOptions } from '../widgets/block-diff/options'; +import { + AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE, + AffineBlockDiffWidgetForPage, +} from '../widgets/block-diff/page'; +import { + AFFINE_BLOCK_DIFF_PLAYGROUND, + AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL, + BlockDiffPlayground, + BlockDiffPlaygroundModal, +} from '../widgets/block-diff/playground'; +import { + AFFINE_EDGELESS_COPILOT_WIDGET, + EdgelessCopilotWidget, +} from '../widgets/edgeless-copilot'; +import { EdgelessCopilotPanel } from '../widgets/edgeless-copilot-panel'; +import { EdgelessCopilotToolbarEntry } from '../widgets/edgeless-copilot-panel/toolbar-entry'; +import { + type EditorEffectElementTag, + editorEffectElementTags, +} from './registry'; +import { registerAISharedEffects } from './shared'; + +const editorRegistries = new WeakSet(); +const editorElements = { + 'ask-ai-icon': AskAIIcon, + 'ask-ai-button': AskAIButton, + 'ask-ai-toolbar-button': AskAIToolbarButton, + 'ask-ai-panel': AskAIPanel, + 'affine-edgeless-ai-chat': EdgelessAIChatBlockComponent, + 'affine-ai-chat': AIChatBlockComponent, + 'ai-chat-block-message': AIChatBlockMessage, + 'ai-chat-block-messages': AIChatBlockMessages, + 'ai-scrollable-text-renderer': AIScrollableTextRenderer, + 'image-placeholder': ImagePlaceholder, + 'chat-image': ChatImage, + 'chat-images': ChatImages, + 'user-info': UserInfo, + 'generating-placeholder': GeneratingPlaceholder, + 'ai-finish-tip': AIFinishTip, + 'ai-panel-divider': AIPanelDivider, + 'ai-panel-answer': AIPanelAnswer, + 'ai-panel-input': AIPanelInput, + 'ai-panel-generating': AIPanelGenerating, + 'ai-panel-error': AIPanelError, + 'ai-block-diff-options': BlockDiffOptions, + [AFFINE_BLOCK_DIFF_PLAYGROUND]: BlockDiffPlayground, + [AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL]: BlockDiffPlaygroundModal, + [AFFINE_AI_PANEL_WIDGET]: AffineAIPanelWidget, + [AFFINE_EDGELESS_COPILOT_WIDGET]: EdgelessCopilotWidget, + [AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK]: AffineBlockDiffWidgetForBlock, + [AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE]: AffineBlockDiffWidgetForPage, + 'edgeless-copilot-panel': EdgelessCopilotPanel, + 'edgeless-copilot-toolbar-entry': EdgelessCopilotToolbarEntry, + 'transcription-block': LitTranscriptionBlock, +} satisfies Record; + +export function registerAIEditorEffects() { + const registry = customElements; + if (editorRegistries.has(registry)) return; + editorRegistries.add(registry); + + registerAISharedEffects(); + componentAiItemEffects(); + + for (const tag of editorEffectElementTags) { + customElements.define(tag, editorElements[tag]); + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/effects/registry.ts b/packages/frontend/core/src/blocksuite/ai/effects/registry.ts new file mode 100644 index 0000000000..f113f393c1 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/effects/registry.ts @@ -0,0 +1,106 @@ +export const sharedEffectElementTags = [ + 'ai-error-wrapper', + 'ai-slides-renderer', + 'ai-answer-wrapper', + 'chat-content-images', + 'chat-content-pure-text', + 'chat-content-rich-text', + 'chat-content-stream-objects', + 'text-renderer', + 'tool-call-card', + 'tool-result-card', + 'tool-call-failed', + 'doc-semantic-search-result', + 'doc-keyword-search-result', + 'doc-read-result', + 'doc-write-tool', + 'web-crawl-tool', + 'web-search-tool', + 'section-edit-tool', + 'doc-compose-tool', + 'code-artifact-tool', + 'code-highlighter', + 'artifact-preview-panel', + 'doc-edit-tool', + 'artifact-skeleton', +] as const; + +export type SharedEffectElementTag = (typeof sharedEffectElementTags)[number]; + +export const editorEffectElementTags = [ + 'ask-ai-icon', + 'ask-ai-button', + 'ask-ai-toolbar-button', + 'ask-ai-panel', + 'affine-edgeless-ai-chat', + 'affine-ai-chat', + 'ai-chat-block-message', + 'ai-chat-block-messages', + 'ai-scrollable-text-renderer', + 'image-placeholder', + 'chat-image', + 'chat-images', + 'user-info', + 'generating-placeholder', + 'ai-finish-tip', + 'ai-panel-divider', + 'ai-panel-answer', + 'ai-panel-input', + 'ai-panel-generating', + 'ai-panel-error', + 'ai-block-diff-options', + 'affine-block-diff-playground', + 'affine-block-diff-playground-modal', + 'affine-ai-panel-widget', + 'affine-edgeless-copilot-widget', + 'affine-block-diff-widget-for-block', + 'affine-block-diff-widget-for-page', + 'edgeless-copilot-panel', + 'edgeless-copilot-toolbar-entry', + 'transcription-block', +] as const; + +export type EditorEffectElementTag = (typeof editorEffectElementTags)[number]; + +export const appEffectElementTags = [ + 'chat-action-list', + 'chat-copy-more', + 'image-preview-grid', + 'action-wrapper', + 'action-image-to-text', + 'action-image', + 'action-make-real', + 'action-mindmap', + 'action-slides', + 'action-text', + 'ai-loading', + 'ai-chat-content', + 'ai-chat-toolbar', + 'ai-session-history', + 'ai-chat-messages', + 'ai-chat-input', + 'ai-chat-add-context', + 'ai-chat-embedding-status-tooltip', + 'ai-chat-composer', + 'chat-panel-chips', + 'ai-history-clear', + 'chat-panel-add-popover', + 'chat-input-preference', + 'chat-panel-candidates-popover', + 'chat-panel-doc-chip', + 'chat-panel-file-chip', + 'chat-panel-tag-chip', + 'chat-panel-collection-chip', + 'chat-panel-selected-chip', + 'chat-panel-attachment-chip', + 'chat-panel-chip', + 'chat-assistant-avatar', + 'chat-message-action', + 'chat-message-assistant', + 'chat-message-user', + 'ai-chat-block-peek-view', + 'date-time', + 'chat-panel-split-view', +] as const; + +export type AppEffectElementTag = (typeof appEffectElementTags)[number]; diff --git a/packages/frontend/core/src/blocksuite/ai/effects/shared.ts b/packages/frontend/core/src/blocksuite/ai/effects/shared.ts new file mode 100644 index 0000000000..ea021a010a --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/effects/shared.ts @@ -0,0 +1,74 @@ +import { effects as tooltipEffects } from '@blocksuite/affine-components/tooltip'; + +import { ArtifactSkeleton } from '../components/ai-artifact-skeleton'; +import { ChatContentImages } from '../components/ai-message-content/images'; +import { ChatContentPureText } from '../components/ai-message-content/pure-text'; +import { ChatContentRichText } from '../components/ai-message-content/rich-text'; +import { ChatContentStreamObjects } from '../components/ai-message-content/stream-objects'; +import { ArtifactPreviewPanel } from '../components/ai-tools/artifacts-preview-panel'; +import { + CodeArtifactTool, + CodeHighlighter, +} from '../components/ai-tools/code-artifact'; +import { DocComposeTool } from '../components/ai-tools/doc-compose'; +import { DocEditTool } from '../components/ai-tools/doc-edit'; +import { DocKeywordSearchResult } from '../components/ai-tools/doc-keyword-search-result'; +import { DocReadResult } from '../components/ai-tools/doc-read-result'; +import { DocSemanticSearchResult } from '../components/ai-tools/doc-semantic-search-result'; +import { DocWriteTool } from '../components/ai-tools/doc-write'; +import { SectionEditTool } from '../components/ai-tools/section-edit'; +import { ToolCallCard } from '../components/ai-tools/tool-call-card'; +import { ToolFailedCard } from '../components/ai-tools/tool-failed-card'; +import { ToolResultCard } from '../components/ai-tools/tool-result-card'; +import { WebCrawlTool } from '../components/ai-tools/web-crawl'; +import { WebSearchTool } from '../components/ai-tools/web-search'; +import { TextRenderer } from '../components/text-renderer'; +import { AIErrorWrapper } from '../messages/error'; +import { AISlidesRenderer } from '../messages/slides-renderer'; +import { AIAnswerWrapper } from '../messages/wrapper'; +import { registerMiniMindmapBlocks } from '../mini-mindmap'; +import { + type SharedEffectElementTag, + sharedEffectElementTags, +} from './registry'; + +const sharedRegistries = new WeakSet(); +const sharedElements = { + 'ai-error-wrapper': AIErrorWrapper, + 'ai-slides-renderer': AISlidesRenderer, + 'ai-answer-wrapper': AIAnswerWrapper, + 'chat-content-images': ChatContentImages, + 'chat-content-pure-text': ChatContentPureText, + 'chat-content-rich-text': ChatContentRichText, + 'chat-content-stream-objects': ChatContentStreamObjects, + 'text-renderer': TextRenderer, + 'tool-call-card': ToolCallCard, + 'tool-result-card': ToolResultCard, + 'tool-call-failed': ToolFailedCard, + 'doc-semantic-search-result': DocSemanticSearchResult, + 'doc-keyword-search-result': DocKeywordSearchResult, + 'doc-read-result': DocReadResult, + 'doc-write-tool': DocWriteTool, + 'web-crawl-tool': WebCrawlTool, + 'web-search-tool': WebSearchTool, + 'section-edit-tool': SectionEditTool, + 'doc-compose-tool': DocComposeTool, + 'code-artifact-tool': CodeArtifactTool, + 'code-highlighter': CodeHighlighter, + 'artifact-preview-panel': ArtifactPreviewPanel, + 'doc-edit-tool': DocEditTool, + 'artifact-skeleton': ArtifactSkeleton, +} satisfies Record; + +export function registerAISharedEffects() { + const registry = customElements; + if (sharedRegistries.has(registry)) return; + sharedRegistries.add(registry); + + registerMiniMindmapBlocks(); + tooltipEffects(); + + for (const tag of sharedEffectElementTags) { + customElements.define(tag, sharedElements[tag]); + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/index.ts b/packages/frontend/core/src/blocksuite/ai/index.ts index 5e42d78c39..f897aca83b 100644 --- a/packages/frontend/core/src/blocksuite/ai/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/index.ts @@ -1,6 +1,5 @@ export * from './_common/config'; export * from './actions'; -export { ChatPanel } from './chat-panel'; export * from './entries'; export * from './entries/edgeless/actions-config'; export * from './messages'; diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/const.ts b/packages/frontend/core/src/blocksuite/ai/utils/history-image-actions.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/ai/chat-panel/const.ts rename to packages/frontend/core/src/blocksuite/ai/utils/history-image-actions.ts diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts index 96c1a10cab..8c7418b6e2 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts @@ -1,10 +1,10 @@ -import { registerAIEffects } from '@affine/core/blocksuite/ai/effects'; +import { registerAIEditorEffects } from '@affine/core/blocksuite/ai/effects/editor'; import { editorEffects } from '@affine/core/blocksuite/editors'; import { registerTemplates } from './register-templates'; editorEffects(); -registerAIEffects(); +registerAIEditorEffects(); registerTemplates(); export * from './blocksuite-editor'; diff --git a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx index 27b85f5eb2..019bbbc640 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx @@ -21,6 +21,7 @@ import { EventSourceService, FetchService, GraphQLService, + ServerService, SubscriptionService, } from '@affine/core/modules/cloud'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; @@ -228,6 +229,7 @@ export const Component = () => { ); content.aiDraftService = framework.get(AIDraftService); content.aiToolsConfigService = framework.get(AIToolsConfigService); + content.serverService = framework.get(ServerService); content.subscriptionService = framework.get(SubscriptionService); content.aiModelService = framework.get(AIModelService); content.onAISubscribe = handleAISubscribe; diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx index 1178dcf30e..02ab7f5aff 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx @@ -1,6 +1,6 @@ import { Scrollable } from '@affine/component'; import { PageDetailLoading } from '@affine/component/page-detail-skeleton'; -import type { AIChatParams, ChatPanel } from '@affine/core/blocksuite/ai'; +import type { AIChatParams } from '@affine/core/blocksuite/ai'; import { AIProvider } from '@affine/core/blocksuite/ai'; import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor'; import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer'; @@ -103,7 +103,6 @@ const DetailPageImpl = memo(function DetailPageImpl() { const isSideBarOpen = useLiveData(workbench.sidebarOpen$); const { appSettings } = useAppSettingHelper(); - const chatPanelRef = useRef(null); const peekView = useService(PeekViewService).peekView; @@ -373,7 +372,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { icon={} unmountOnInactive={false} > - + )} diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat-panel-session.spec.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat-panel-session.spec.ts new file mode 100644 index 0000000000..dbdc89904f --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat-panel-session.spec.ts @@ -0,0 +1,147 @@ +/* eslint-disable rxjs/finnish */ +import type { CopilotChatHistoryFragment } from '@affine/graphql'; +import { describe, expect, test, vi } from 'vitest'; + +import { + resolveInitialSession, + type SessionService, + type WorkbenchLike, +} from './chat-panel-session'; + +const createWorkbench = (search: string) => { + const updateQueryString = vi.fn(); + const workbench = { + location$: { value: { search } }, + activeView$: { value: { updateQueryString } }, + } satisfies WorkbenchLike; + + return { workbench, updateQueryString }; +}; + +const doc = { id: 'doc-1', workspace: { id: 'ws-1' } }; + +test('returns undefined without session service or doc', async () => { + await expect( + resolveInitialSession({ sessionService: null, doc, workbench: null }) + ).resolves.toBeUndefined(); + await expect( + resolveInitialSession({ + sessionService: { + getSessions: vi.fn(), + getSession: vi.fn(), + }, + doc: null, + workbench: null, + }) + ).resolves.toBeUndefined(); +}); + +describe('resolveInitialSession', () => { + test('prefers pinned session and clears sessionId from url', async () => { + const pinnedSession = { + sessionId: 'pinned-session', + pinned: true, + } as CopilotChatHistoryFragment; + + const sessionService: SessionService = { + getSessions: vi.fn().mockResolvedValueOnce([pinnedSession]), + getSession: vi.fn(), + }; + + const { workbench, updateQueryString } = createWorkbench( + '?sessionId=from-url' + ); + + const result = await resolveInitialSession({ + sessionService, + doc, + workbench, + }); + + expect(result).toBe(pinnedSession); + expect(updateQueryString).toHaveBeenCalledWith( + { sessionId: undefined }, + { replace: true } + ); + expect(sessionService.getSession).not.toHaveBeenCalled(); + }); + + test('loads session from url when no pinned session', async () => { + const sessionFromUrl = { + sessionId: 'url-session', + pinned: false, + } as CopilotChatHistoryFragment; + + const sessionService: SessionService = { + getSessions: vi.fn().mockResolvedValueOnce([]), + getSession: vi.fn().mockResolvedValueOnce(sessionFromUrl), + }; + + const { workbench, updateQueryString } = createWorkbench( + '?sessionId=url-session' + ); + + const result = await resolveInitialSession({ + sessionService, + doc, + workbench, + }); + + expect(result).toBe(sessionFromUrl); + expect(sessionService.getSession).toHaveBeenCalledWith( + doc.workspace.id, + 'url-session' + ); + expect(updateQueryString).toHaveBeenCalledWith( + { sessionId: undefined }, + { replace: true } + ); + }); + + test('falls back to latest doc session', async () => { + const docSession = { + sessionId: 'doc-session', + pinned: false, + } as CopilotChatHistoryFragment; + + const sessionService: SessionService = { + getSessions: vi + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([docSession]), + getSession: vi.fn(), + }; + + const { workbench } = createWorkbench(''); + + const result = await resolveInitialSession({ + sessionService, + doc, + workbench, + }); + + expect(result).toBe(docSession); + expect(sessionService.getSessions).toHaveBeenCalledWith( + doc.workspace.id, + doc.id, + { action: false, fork: false, limit: 1 } + ); + }); + + test('returns null when url session is missing', async () => { + const sessionService: SessionService = { + getSessions: vi.fn().mockResolvedValueOnce([]), + getSession: vi.fn().mockResolvedValueOnce(null), + }; + + const { workbench } = createWorkbench('?sessionId=missing'); + + const result = await resolveInitialSession({ + sessionService, + doc, + workbench, + }); + + expect(result).toBeNull(); + }); +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat-panel-session.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat-panel-session.ts new file mode 100644 index 0000000000..16cf80095d --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat-panel-session.ts @@ -0,0 +1,108 @@ +/* eslint-disable rxjs/finnish */ +import type { CopilotChatHistoryFragment } from '@affine/graphql'; + +type SessionListOptions = { + pinned?: boolean; + action?: boolean; + fork?: boolean; + limit?: number; +}; + +export interface SessionService { + getSessions: ( + workspaceId: string, + docId?: string, + options?: SessionListOptions + ) => Promise; + getSession: ( + workspaceId: string, + sessionId: string + ) => Promise; +} + +export interface WorkbenchLike { + location$: { + value: { + search: string; + }; + }; + activeView$: { + value: { + updateQueryString: ( + patch: Record, + options?: { replace?: boolean } + ) => void; + }; + }; +} + +export interface DocLike { + id: string; + workspace: { + id: string; + }; +} + +export const getSessionIdFromUrl = (workbench?: WorkbenchLike | null) => { + if (!workbench) { + return undefined; + } + const searchParams = new URLSearchParams(workbench.location$.value.search); + const sessionId = searchParams.get('sessionId'); + if (sessionId) { + workbench.activeView$.value.updateQueryString( + { sessionId: undefined }, + { replace: true } + ); + } + return sessionId ?? undefined; +}; + +export const resolveInitialSession = async ({ + sessionService, + doc, + workbench, +}: { + sessionService?: SessionService | null; + doc?: DocLike | null; + workbench?: WorkbenchLike | null; +}): Promise => { + if (!sessionService || !doc) { + return undefined; + } + + const sessionId = getSessionIdFromUrl(workbench); + + const pinSessions = await sessionService.getSessions( + doc.workspace.id, + undefined, + { + pinned: true, + limit: 1, + } + ); + + if (Array.isArray(pinSessions) && pinSessions[0]) { + return pinSessions[0]; + } + + if (sessionId) { + const session = await sessionService.getSession( + doc.workspace.id, + sessionId + ); + return session ?? null; + } + + const docSessions = await sessionService.getSessions( + doc.workspace.id, + doc.id, + { + action: false, + fork: false, + limit: 1, + } + ); + + return docSessions?.[0] ?? null; +}; diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.css.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.css.ts index 8dff59d450..94016894a1 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.css.ts @@ -1,6 +1,100 @@ -import { style } from '@vanilla-extract/css'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { globalStyle, style } from '@vanilla-extract/css'; + export const root = style({ display: 'flex', + flexDirection: 'column', height: '100%', width: '100%', + userSelect: 'text', +}); + +export const container = style({ + display: 'flex', + flexDirection: 'column', + height: '100%', +}); + +export const header = style({ + background: 'var(--affine-background-primary-color)', + position: 'relative', + padding: '8px var(--h-padding, 16px)', + width: '100%', + height: '36px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + zIndex: 1, +}); + +export const title = style({ + fontSize: '14px', + fontWeight: 500, + color: 'var(--affine-text-secondary-color)', + display: 'flex', + alignItems: 'center', +}); + +export const playground = style({ + cursor: 'pointer', + padding: '2px', + marginLeft: '8px', + marginRight: 'auto', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +export const content = style({ + flexGrow: 1, + height: 0, + minHeight: 0, + display: 'flex', + flexDirection: 'column', + width: '100%', +}); + +export const loadingContainer = style({ + position: 'relative', + padding: '44px 0 166px 0', + height: '100%', + display: 'flex', + alignItems: 'center', +}); + +export const loading = style({ + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '12px', +}); + +export const loadingTitle = style({ + fontWeight: 600, + fontSize: 'var(--affine-font-sm)', + color: 'var(--affine-text-secondary-color)', +}); + +export const loadingIcon = style({ + width: '44px', + height: '44px', + color: 'var(--affine-icon-secondary)', +}); + +globalStyle(`${playground} svg`, { + width: '18px', + height: '18px', + color: 'var(--affine-text-secondary-color)', +}); + +globalStyle(`${playground}:hover svg`, { + color: cssVarV2('icon/activated'), +}); + +globalStyle(`${content} > ai-chat-content`, { + flexGrow: 1, + height: 0, + minHeight: 0, + width: '100%', }); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx index 97ae4d35a6..5e21223358 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/chat.tsx @@ -1,5 +1,14 @@ import { useConfirmModal } from '@affine/component'; -import { AIProvider, ChatPanel } from '@affine/core/blocksuite/ai'; +import { AIProvider } from '@affine/core/blocksuite/ai'; +import type { AppSidebarConfig } from '@affine/core/blocksuite/ai/chat-panel/chat-config'; +import { + AIChatContent, + type ChatContextValue, +} from '@affine/core/blocksuite/ai/components/ai-chat-content'; +import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages'; +import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar'; +import { createPlaygroundModal } from '@affine/core/blocksuite/ai/components/playground/modal'; +import { registerAIAppEffects } from '@affine/core/blocksuite/ai/effects/app'; import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor'; import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service'; import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config'; @@ -12,48 +21,49 @@ import { import { AIModelService } from '@affine/core/modules/ai-button/services/models'; import { ServerService, SubscriptionService } from '@affine/core/modules/cloud'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; +import { useSignalValue } from '@affine/core/modules/doc-info/utils'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { PeekViewService } from '@affine/core/modules/peek-view'; import { AppThemeService } from '@affine/core/modules/theme'; import { WorkbenchService } from '@affine/core/modules/workbench'; +import type { + ContextEmbedStatus, + CopilotChatHistoryFragment, + UpdateChatSessionInput, +} from '@affine/graphql'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; import { DocModeProvider } from '@blocksuite/affine/shared/services'; import { createSignalFromObservable } from '@blocksuite/affine/shared/utils'; +import { CenterPeekIcon, Logo1Icon } from '@blocksuite/icons/rc'; +import type { Signal } from '@preact/signals-core'; import { useFramework, useService } from '@toeverything/infra'; -import { forwardRef, useEffect, useRef, useState } from 'react'; +import { html } from 'lit'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import * as styles from './chat.css'; +import { + resolveInitialSession, + type WorkbenchLike, +} from './chat-panel-session'; + +registerAIAppEffects(); export interface SidebarTabProps { editor: AffineEditorContainer | null; onLoad?: ((component: HTMLElement) => void) | null; } -// A wrapper for CopilotPanel -export const EditorChatPanel = forwardRef(function EditorChatPanel( - { editor, onLoad }: SidebarTabProps, - ref: React.ForwardedRef -) { - const chatPanelRef = useRef(null); - const containerRef = useRef(null); - const workbench = useService(WorkbenchService).workbench; +export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => { const framework = useFramework(); + const workbench = useService(WorkbenchService).workbench; - useEffect(() => { - if (onLoad && chatPanelRef.current) { - (chatPanelRef.current as ChatPanel).updateComplete - .then(() => { - if (ref) { - if (typeof ref === 'function') { - ref(chatPanelRef.current); - } else { - ref.current = chatPanelRef.current; - } - } - }) - .catch(console.error); - } - }, [onLoad, ref]); + const { closeConfirmModal, openConfirmModal } = useConfirmModal(); + const notificationService = useMemo( + () => new NotificationServiceImpl(closeConfirmModal, openConfirmModal), + [closeConfirmModal, openConfirmModal] + ); + const specs = useAISpecs(); + const handleAISubscribe = useAISubscribe(); const { docDisplayConfig, @@ -61,101 +71,477 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel( reasoningConfig, playgroundConfig, } = useAIChatConfig(); - const confirmModal = useConfirmModal(); - const specs = useAISpecs(); - const handleAISubscribe = useAISubscribe(); + const playgroundVisible = useSignalValue(playgroundConfig.visible) ?? false; + + const [session, setSession] = useState< + CopilotChatHistoryFragment | null | undefined + >(undefined); + const [embeddingProgress, setEmbeddingProgress] = useState<[number, number]>([ + 0, 0, + ]); + const [status, setStatus] = useState('idle'); + const [hasPinned, setHasPinned] = useState(false); + + const [chatContent, setChatContent] = useState(null); + const [chatToolbar, setChatToolbar] = useState(null); + const [isBodyProvided, setIsBodyProvided] = useState(false); + const [isHeaderProvided, setIsHeaderProvided] = useState(false); + + const chatContainerRef = useRef(null); + const chatToolbarContainerRef = useRef(null); + const contentKeyRef = useRef(null); + const lastDocIdRef = useRef(null); + + const doc = editor?.doc; + const host = editor?.host; + + const appSidebarConfig = useMemo(() => { + return { + getWidth: () => + createSignalFromObservable( + workbench.sidebarWidth$.asObservable(), + 0 + ), + isOpen: () => + createSignalFromObservable( + workbench.sidebarOpen$.asObservable(), + true + ), + }; + }, [workbench]); + + const [sidebarWidthSignal, setSidebarWidthSignal] = + useState>(); useEffect(() => { - if (!editor || !editor.host) return; + const { signal, cleanup } = appSidebarConfig.getWidth(); + setSidebarWidthSignal(signal); + return cleanup; + }, [appSidebarConfig]); - if (!chatPanelRef.current) { - chatPanelRef.current = new ChatPanel(); - chatPanelRef.current.host = editor.host; - chatPanelRef.current.doc = editor.doc; + const resetPanel = useCallback(() => { + setSession(undefined); + setEmbeddingProgress([0, 0]); + setHasPinned(false); + }, []); - const workbench = framework.get(WorkbenchService).workbench; - chatPanelRef.current.appSidebarConfig = { - getWidth: () => { - const width$ = workbench.sidebarWidth$; - return createSignalFromObservable(width$, 0); - }, - isOpen: () => { - const open$ = workbench.sidebarOpen$; - return createSignalFromObservable(open$, true); - }, - }; + const initPanel = useCallback(async () => { + try { + const nextSession = await resolveInitialSession({ + sessionService: AIProvider.session ?? undefined, + doc, + workbench: workbench as WorkbenchLike, + }); - chatPanelRef.current.docDisplayConfig = docDisplayConfig; - chatPanelRef.current.searchMenuConfig = searchMenuConfig; - chatPanelRef.current.reasoningConfig = reasoningConfig; - chatPanelRef.current.playgroundConfig = playgroundConfig; - chatPanelRef.current.extensions = specs; - chatPanelRef.current.serverService = framework.get(ServerService); - chatPanelRef.current.affineFeatureFlagService = - framework.get(FeatureFlagService); - chatPanelRef.current.affineWorkspaceDialogService = framework.get( - WorkspaceDialogService + if (nextSession === undefined) { + return; + } + + setSession(nextSession); + setHasPinned(!!nextSession?.pinned); + } catch (error) { + console.error(error); + } + }, [doc, workbench]); + + const createSession = useCallback( + async (options: Partial = {}) => { + if (session || !AIProvider.session || !doc) { + return session ?? undefined; + } + const sessionId = await AIProvider.session.createSession({ + docId: doc.id, + workspaceId: doc.workspace.id, + promptName: 'Chat With AFFiNE AI', + reuseLatestChat: false, + ...options, + }); + if (sessionId) { + const nextSession = await AIProvider.session.getSession( + doc.workspace.id, + sessionId + ); + setSession(nextSession ?? null); + return nextSession ?? undefined; + } + return session ?? undefined; + }, + [doc, session] + ); + + const updateSession = useCallback( + async (options: UpdateChatSessionInput) => { + if (!AIProvider.session || !doc) { + return undefined; + } + await AIProvider.session.updateSession(options); + const nextSession = await AIProvider.session.getSession( + doc.workspace.id, + options.sessionId ); - chatPanelRef.current.affineWorkbenchService = - framework.get(WorkbenchService); - chatPanelRef.current.affineThemeService = framework.get(AppThemeService); - chatPanelRef.current.peekViewService = framework.get(PeekViewService); - chatPanelRef.current.notificationService = new NotificationServiceImpl( - confirmModal.closeConfirmModal, - confirmModal.openConfirmModal - ); - chatPanelRef.current.aiDraftService = framework.get(AIDraftService); - chatPanelRef.current.aiToolsConfigService = - framework.get(AIToolsConfigService); - chatPanelRef.current.subscriptionService = - framework.get(SubscriptionService); - chatPanelRef.current.aiModelService = framework.get(AIModelService); - chatPanelRef.current.onAISubscribe = handleAISubscribe; + setSession(nextSession ?? null); + return nextSession ?? undefined; + }, + [doc] + ); - containerRef.current?.append(chatPanelRef.current); - } else { - chatPanelRef.current.host = editor.host; - chatPanelRef.current.doc = editor.doc; + const newSession = useCallback(() => { + resetPanel(); + requestAnimationFrame(() => { + setSession(null); + }); + }, [resetPanel]); + + const openSession = useCallback( + async (sessionId: string) => { + if (session?.sessionId === sessionId || !AIProvider.session || !doc) { + return; + } + resetPanel(); + const nextSession = await AIProvider.session.getSession( + doc.workspace.id, + sessionId + ); + setSession(nextSession ?? null); + }, + [doc, resetPanel, session?.sessionId] + ); + + const openDoc = useCallback( + async (docId: string, sessionId?: string) => { + if (!doc) { + return; + } + if (doc.id === docId) { + if (session?.sessionId === sessionId || session?.pinned) { + return; + } + if (sessionId) { + await openSession(sessionId); + } + return; + } + if (session?.pinned || !sessionId) { + workbench.open(`/${docId}`, { at: 'active' }); + return; + } + workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' }); + }, + [doc, openSession, session?.pinned, session?.sessionId, workbench] + ); + + const deleteSession = useCallback( + async (sessionToDelete: BlockSuitePresets.AIRecentSession) => { + if (!AIProvider.histories) { + return; + } + const confirm = await notificationService.confirm({ + title: 'Delete this history?', + message: + 'Do you want to delete this AI conversation history? Once deleted, it cannot be recovered.', + confirmText: 'Delete', + cancelText: 'Cancel', + }); + if (confirm) { + await AIProvider.histories.cleanup( + sessionToDelete.workspaceId, + sessionToDelete.docId || undefined, + [sessionToDelete.sessionId] + ); + if (sessionToDelete.sessionId === session?.sessionId) { + newSession(); + } + } + }, + [newSession, notificationService, session?.sessionId] + ); + + const togglePin = useCallback(async () => { + const pinned = !session?.pinned; + setHasPinned(true); + if (!session) { + await createSession({ pinned }); + return; + } + setSession(prev => (prev ? { ...prev, pinned } : prev)); + await updateSession({ + sessionId: session.sessionId, + pinned, + }); + }, [createSession, session, updateSession]); + + const rebindSession = useCallback(async () => { + if (!session || !doc) { + return; + } + if (session.docId !== doc.id) { + await updateSession({ + sessionId: session.sessionId, + docId: doc.id, + }); + } + }, [doc, session, updateSession]); + + const onEmbeddingProgressChange = useCallback( + (count: Record) => { + const total = count.finished + count.processing + count.failed; + setEmbeddingProgress([count.finished, total]); + }, + [] + ); + + const onContextChange = useCallback( + (context: Partial) => { + setStatus(context.status ?? 'idle'); + if (context.status === 'success') { + rebindSession().catch(console.error); + } + }, + [rebindSession] + ); + + useEffect(() => { + if (session !== undefined) { + return; + } + if (chatContent) { + chatContent.remove(); + setChatContent(null); + } + if (chatToolbar) { + chatToolbar.remove(); + setChatToolbar(null); + } + }, [chatContent, chatToolbar, session]); + + useEffect(() => { + const subscription = AIProvider.slots.userInfo.subscribe(() => { + resetPanel(); + initPanel().catch(console.error); + }); + return () => subscription.unsubscribe(); + }, [initPanel, resetPanel]); + + useEffect(() => { + const docId = doc?.id; + if (!docId) { + return; + } + if ( + lastDocIdRef.current && + lastDocIdRef.current !== docId && + !session?.pinned + ) { + resetPanel(); + } + lastDocIdRef.current = docId; + }, [doc?.id, resetPanel, session?.pinned]); + + useEffect(() => { + if (!doc || session !== undefined) { + return; } + let cancelled = false; + let timerId: ReturnType | null = null; + + const tryInit = () => { + if (cancelled || session !== undefined) { + return; + } + // Session service may be registered after the panel mounts. + if (AIProvider.session) { + initPanel().catch(console.error); + return; + } + timerId = setTimeout(tryInit, 200); + }; + + tryInit(); + + return () => { + cancelled = true; + if (timerId) { + clearTimeout(timerId); + } + }; + }, [doc, initPanel, session]); + + const contentKey = hasPinned + ? (session?.sessionId ?? doc?.id ?? 'chat-panel') + : (doc?.id ?? 'chat-panel'); + + useEffect(() => { + if (!chatContent) { + contentKeyRef.current = contentKey; + return; + } + if (contentKeyRef.current && contentKeyRef.current !== contentKey) { + chatContent.remove(); + setChatContent(null); + } + contentKeyRef.current = contentKey; + }, [chatContent, contentKey]); + + useEffect(() => { + if (!isBodyProvided || !chatContainerRef.current || !doc || !host) { + return; + } + if (session === undefined) { + return; + } + + let content = chatContent; + + if (!content) { + content = new AIChatContent(); + } + + content.host = host; + content.session = session; + content.createSession = createSession; + content.workspaceId = doc.workspace.id; + content.docId = doc.id; + content.reasoningConfig = reasoningConfig; + content.searchMenuConfig = searchMenuConfig; + content.docDisplayConfig = docDisplayConfig; + content.extensions = specs; + content.serverService = framework.get(ServerService); + content.affineFeatureFlagService = framework.get(FeatureFlagService); + content.affineWorkspaceDialogService = framework.get( + WorkspaceDialogService + ); + content.affineThemeService = framework.get(AppThemeService); + content.notificationService = notificationService; + content.aiDraftService = framework.get(AIDraftService); + content.aiToolsConfigService = framework.get(AIToolsConfigService); + content.peekViewService = framework.get(PeekViewService); + content.subscriptionService = framework.get(SubscriptionService); + content.aiModelService = framework.get(AIModelService); + content.onAISubscribe = handleAISubscribe; + content.onEmbeddingProgressChange = onEmbeddingProgressChange; + content.onContextChange = onContextChange; + content.width = sidebarWidthSignal; + content.onOpenDoc = (docId: string, sessionId?: string) => { + openDoc(docId, sessionId).catch(console.error); + }; + + if (!chatContent) { + chatContainerRef.current.append(content); + setChatContent(content); + onLoad?.(content); + } + }, [ + chatContent, + createSession, + doc, + docDisplayConfig, + framework, + handleAISubscribe, + host, + isBodyProvided, + notificationService, + onContextChange, + onEmbeddingProgressChange, + onLoad, + openDoc, + reasoningConfig, + searchMenuConfig, + session, + sidebarWidthSignal, + specs, + ]); + + useEffect(() => { + if (!isHeaderProvided || !chatToolbarContainerRef.current || !doc) { + return; + } + if (session === undefined) { + return; + } + + let tool = chatToolbar; + + if (!tool) { + tool = new AIChatToolbar(); + } + + tool.session = session; + tool.workspaceId = doc.workspace.id; + tool.docId = doc.id; + tool.status = status; + tool.docDisplayConfig = docDisplayConfig; + tool.notificationService = notificationService; + tool.onNewSession = newSession; + tool.onTogglePin = togglePin; + tool.onOpenSession = (sessionId: string) => { + openSession(sessionId).catch(console.error); + }; + tool.onOpenDoc = (docId: string, sessionId: string) => { + openDoc(docId, sessionId).catch(console.error); + }; + tool.onSessionDelete = ( + sessionToDelete: BlockSuitePresets.AIRecentSession + ) => { + deleteSession(sessionToDelete).catch(console.error); + }; + + if (!chatToolbar) { + chatToolbarContainerRef.current.append(tool); + setChatToolbar(tool); + } + }, [ + chatToolbar, + deleteSession, + doc, + docDisplayConfig, + isHeaderProvided, + newSession, + notificationService, + openDoc, + openSession, + session, + status, + togglePin, + ]); + + useEffect(() => { + if (!editor?.host || !chatContent) { + return; + } const docModeService = editor.host.std.get(DocModeProvider); const refNodeService = editor.host.std.getOptional(RefNodeSlotsProvider); const disposable = [ - refNodeService?.docLinkClicked.subscribe(({ host }) => { - if (host === editor.host) { - (chatPanelRef.current as ChatPanel).doc = editor.doc; + refNodeService?.docLinkClicked.subscribe(({ host: clickedHost }) => { + if (clickedHost === editor.host) { + chatContent.docId = editor.doc.id; } }), docModeService?.onPrimaryModeChange(() => { - if (!editor.host) return; - (chatPanelRef.current as ChatPanel).host = editor.host; + if (!editor.host) { + return; + } + chatContent.host = editor.host; }, editor.doc.id), ]; - return () => disposable.forEach(d => d?.unsubscribe()); - }, [ - docDisplayConfig, - editor, - framework, - searchMenuConfig, - reasoningConfig, - playgroundConfig, - confirmModal, - specs, - handleAISubscribe, - ]); + return () => disposable.forEach(item => item?.unsubscribe()); + }, [chatContent, editor]); const [autoResized, setAutoResized] = useState(false); useEffect(() => { - // after auto expanded first time, do not auto expand again(even if user manually resized) - if (autoResized) return; + if (autoResized) { + return; + } const subscription = AIProvider.slots.previewPanelOpenChange.subscribe( open => { - if (!open) return; + if (!open) { + return; + } const sidebarWidth = workbench.sidebarWidth$.value; - const MIN_SIDEBAR_WIDTH = 1080; - if (!sidebarWidth || sidebarWidth < MIN_SIDEBAR_WIDTH) { - workbench.setSidebarWidth(MIN_SIDEBAR_WIDTH); + const minSidebarWidth = 1080; + if (!sidebarWidth || sidebarWidth < minSidebarWidth) { + workbench.setSidebarWidth(minSidebarWidth); setAutoResized(true); } } @@ -165,5 +551,99 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel( }; }, [autoResized, workbench]); - return
; -}); + const openPlayground = useCallback(() => { + if (!doc || !host) { + return; + } + const playgroundContent = html` + + `; + + createPlaygroundModal(playgroundContent, 'AI Playground'); + }, [ + appSidebarConfig, + doc, + docDisplayConfig, + framework, + host, + notificationService, + playgroundConfig, + reasoningConfig, + searchMenuConfig, + specs, + ]); + + const onChatContainerRef = useCallback((node: HTMLDivElement) => { + if (!node) { + return; + } + setIsBodyProvided(true); + chatContainerRef.current = node; + }, []); + + const onChatToolContainerRef = useCallback((node: HTMLDivElement) => { + if (!node) { + return; + } + setIsHeaderProvided(true); + chatToolbarContainerRef.current = node; + }, []); + + const isEmbedding = + embeddingProgress[1] > 0 && embeddingProgress[0] < embeddingProgress[1]; + const [done, total] = embeddingProgress; + const isInitialized = session !== undefined; + + return ( +
+ {!isInitialized ? ( +
+
+ +
+ AFFiNE AI is loading history... +
+
+
+ ) : ( +
+
+
+ {isEmbedding ? ( + + Embedding {done}/{total} + + ) : ( + 'AFFiNE AI' + )} +
+ {playgroundVisible ? ( +
+ +
+ ) : null} +
+
+
+
+ )} +
+ ); +}; diff --git a/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx b/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx index 57c5ef1c3b..b71c9bb612 100644 --- a/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/ai-chat-block-peek-view/index.tsx @@ -1,6 +1,7 @@ import { toReactNode } from '@affine/component'; import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/ai'; import type { AIChatBlockModel } from '@affine/core/blocksuite/ai/blocks/ai-chat-block/model/ai-chat-model'; +import { registerAIAppEffects } from '@affine/core/blocksuite/ai/effects/app'; import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config'; import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe'; import { @@ -15,6 +16,8 @@ import type { EditorHost } from '@blocksuite/affine/std'; import { useFramework } from '@toeverything/infra'; import { useMemo } from 'react'; +registerAIAppEffects(); + export type AIChatBlockPeekViewProps = { model: AIChatBlockModel; host: EditorHost; diff --git a/tests/affine-local/e2e/ai-land.spec.ts b/tests/affine-local/e2e/ai-land.spec.ts index 175c525eac..e7f424f4ae 100644 --- a/tests/affine-local/e2e/ai-land.spec.ts +++ b/tests/affine-local/e2e/ai-land.spec.ts @@ -13,5 +13,5 @@ test('Click ai-land icon', async ({ page }) => { await clickNewPageButton(page); await page.locator('[data-testid=ai-island]').click(); - await expect(page.locator('chat-panel')).toBeVisible(); + await expect(page.getByTestId('chat-panel-input-container')).toBeVisible(); }); diff --git a/yarn.lock b/yarn.lock index d4c0b0a2f6..068dad5a19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -457,6 +457,7 @@ __metadata: fuse.js: "npm:^7.0.0" graphemer: "npm:^1.4.0" graphql: "npm:^16.9.0" + happy-dom: "npm:^20.3.0" history: "npm:^5.3.0" idb: "npm:^8.0.0" idb-keyval: "npm:^6.2.2" @@ -18961,7 +18962,7 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10": +"@types/ws@npm:^8.0.0, @types/ws@npm:^8.18.1, @types/ws@npm:^8.5.10": version: 8.18.1 resolution: "@types/ws@npm:8.18.1" dependencies: @@ -26694,14 +26695,16 @@ __metadata: languageName: node linkType: hard -"happy-dom@npm:^20.0.0": - version: 20.0.7 - resolution: "happy-dom@npm:20.0.7" +"happy-dom@npm:^20.0.0, happy-dom@npm:^20.3.0": + version: 20.3.0 + resolution: "happy-dom@npm:20.3.0" dependencies: "@types/node": "npm:^20.0.0" "@types/whatwg-mimetype": "npm:^3.0.2" + "@types/ws": "npm:^8.18.1" whatwg-mimetype: "npm:^3.0.0" - checksum: 10/1161bfe8fcac0fd093b8fbe8af29e8a6525437f17424be97b21401f0741e6473541b30b080066a32990b884eae4b83a1fd1f948a52607975bc98d1b90b394509 + ws: "npm:^8.18.3" + checksum: 10/aade5560110eeaad502679a314f4d3b8658fe4697c1914d0de09ec6a176611ad0c8953f0aa559c56d1afabe0ab8465cde7b4811006df6eb3b54c5e5ea4169b29 languageName: node linkType: hard @@ -38868,9 +38871,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.18.2": - version: 8.18.3 - resolution: "ws@npm:8.18.3" +"ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.18.2, ws@npm:^8.18.3": + version: 8.19.0 + resolution: "ws@npm:8.19.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -38879,7 +38882,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6 + checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b languageName: node linkType: hard