diff --git a/blocksuite/affine/shared/src/services/notification-service.ts b/blocksuite/affine/shared/src/services/notification-service.ts index da269d88d..fe765bd96 100644 --- a/blocksuite/affine/shared/src/services/notification-service.ts +++ b/blocksuite/affine/shared/src/services/notification-service.ts @@ -40,7 +40,6 @@ export interface NotificationService { }[]; onClose?: () => void; }): void; - /** * Notify with undo action, it is a helper function to notify with undo action. * And the notification card will be closed when undo action is triggered by shortcut key or other ways. @@ -55,13 +54,16 @@ export const NotificationProvider = createIdentifier( ); export function NotificationExtension( - notificationService: Omit + notificationService: NotificationService ): ExtensionType { return { setup: di => { di.addImpl(NotificationProvider, provider => { return { - ...notificationService, + notify: notificationService.notify, + toast: notificationService.toast, + confirm: notificationService.confirm, + prompt: notificationService.prompt, notifyWithUndoAction: options => { notifyWithUndoActionImpl( provider, diff --git a/blocksuite/framework/std/src/scope/std-scope.ts b/blocksuite/framework/std/src/scope/std-scope.ts index c0310037f..bf01b7e77 100644 --- a/blocksuite/framework/std/src/scope/std-scope.ts +++ b/blocksuite/framework/std/src/scope/std-scope.ts @@ -31,7 +31,7 @@ export interface BlockStdOptions { extensions: ExtensionType[]; } -const internalExtensions = [ +export const internalExtensions = [ ServiceManager, CommandManager, UIEventDispatcher, diff --git a/packages/frontend/core/src/blocksuite/ai/actions/types.ts b/packages/frontend/core/src/blocksuite/ai/actions/types.ts index 8de72a365..54f995b81 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/types.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/types.ts @@ -1,4 +1,5 @@ import type { + AddContextFileInput, ContextMatchedDocChunk, ContextMatchedFileChunk, ContextWorkspaceEmbeddingStatus, @@ -295,10 +296,7 @@ declare global { }) => Promise; addContextFile: ( file: File, - options: { - contextId: string; - blobId: string; - } + options: AddContextFileInput ) => Promise; removeContextFile: (options: { contextId: string; diff --git a/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts b/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts index 2b73668eb..4ae8414d1 100644 --- a/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts +++ b/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts @@ -1,5 +1,9 @@ import type { TextRendererOptions } from '@affine/core/blocksuite/ai/components/text-renderer'; import type { EditorHost } from '@blocksuite/affine/std'; +import { + NotificationProvider, + ThemeProvider, +} from '@blocksuite/affine-shared/services'; import { css, html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -65,6 +69,7 @@ export class AIChatBlockMessage extends LitElement { } private renderStreamObjects(answer: StreamObject[]) { + const notificationService = this.host.std.get(NotificationProvider); return html``; } private renderRichText(text: string) { return html``; } 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 da5731fa2..1e8af8c95 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 @@ -1,6 +1,7 @@ import { WithDisposable } from '@blocksuite/affine/global/lit'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import type { EditorHost } from '@blocksuite/affine/std'; +import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { ArrowDownBigIcon as ArrowDownIcon, ArrowUpBigIcon as ArrowUpIcon, @@ -163,16 +164,18 @@ export class ActionWrapper extends WithDisposable(LitElement) { >` : nothing} ${answer - ? createTextRenderer(this.host, { + ? createTextRenderer({ customHeading: true, testId: 'chat-message-action-answer', + theme: this.host.std.get(ThemeProvider).app$, })(answer) : nothing} ${originalText ? html`
Prompt
- ${createTextRenderer(this.host, { + ${createTextRenderer({ customHeading: true, testId: 'chat-message-action-prompt', + theme: this.host.std.get(ThemeProvider).app$, })(item.messages[0].content + originalText)}` : nothing} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/text.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/text.ts index 206d7384e..2576f9dd0 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/text.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/text.ts @@ -3,6 +3,7 @@ import './action-wrapper'; import { WithDisposable } from '@blocksuite/affine/global/lit'; import { unsafeCSSVar } from '@blocksuite/affine/shared/theme'; import type { EditorHost } from '@blocksuite/affine/std'; +import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { css, html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -57,8 +58,9 @@ export class ActionText extends WithDisposable(LitElement) { class="original-text" data-testid="original-text" > - ${createTextRenderer(this.host, { + ${createTextRenderer({ customHeading: true, + theme: this.host.std.get(ThemeProvider).app$, })(originalText)} `; diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts index ad0f7ba87..3b34c3912 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -1,5 +1,6 @@ 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 { WorkbenchService } from '@affine/core/modules/workbench'; import type { ContextEmbedStatus, @@ -7,7 +8,7 @@ import type { UpdateChatSessionInput, } from '@affine/graphql'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; -import { NotificationProvider } from '@blocksuite/affine/shared/services'; +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'; @@ -125,6 +126,12 @@ export class ChatPanel extends SignalWatcher( @property({ attribute: false }) accessor affineWorkbenchService!: WorkbenchService; + @property({ attribute: false }) + accessor affineThemeService!: AppThemeService; + + @property({ attribute: false }) + accessor notificationService!: NotificationService; + @state() accessor session: CopilotChatHistoryFragment | null | undefined; @@ -144,7 +151,6 @@ export class ChatPanel extends SignalWatcher( private get chatTitle() { const [done, total] = this.embeddingProgress; const isEmbedding = total > 0 && done < total; - const notification = this.host.std.getOptional(NotificationProvider); return html`
@@ -170,7 +176,7 @@ export class ChatPanel extends SignalWatcher( .onOpenSession=${this.openSession} .onOpenDoc=${this.openDoc} .docDisplayConfig=${this.docDisplayConfig} - .notification=${notification} + .notificationService=${this.notificationService} > `; } @@ -371,6 +377,8 @@ export class ChatPanel extends SignalWatcher( .docDisplayConfig=${this.docDisplayConfig} .extensions=${this.extensions} .affineFeatureFlagService=${this.affineFeatureFlagService} + .affineThemeService=${this.affineThemeService} + .notificationService=${this.notificationService} > `; @@ -444,6 +452,8 @@ export class ChatPanel extends SignalWatcher( .extensions=${this.extensions} .affineFeatureFlagService=${this.affineFeatureFlagService} .affineWorkspaceDialogService=${this.affineWorkspaceDialogService} + .affineThemeService=${this.affineThemeService} + .notificationService=${this.notificationService} .onEmbeddingProgressChange=${this.onEmbeddingProgressChange} .onContextChange=${this.onContextChange} .width=${this.sidebarWidth} diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts index 0ef557c9a..ec53ce1a2 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts @@ -1,10 +1,12 @@ import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import type { AppThemeService } from '@affine/core/modules/theme'; import type { CopilotChatHistoryFragment } from '@affine/graphql'; import { WithDisposable } from '@blocksuite/affine/global/lit'; import { isInsidePageEditor } from '@blocksuite/affine/shared/utils'; import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; import type { ExtensionType } from '@blocksuite/affine/store'; +import type { NotificationService } from '@blocksuite/affine-shared/services'; import type { Signal } from '@preact/signals-core'; import { css, html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; @@ -35,9 +37,6 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor host: EditorHost | null | undefined; - @property({ attribute: false }) - accessor docId: string | undefined; - @property({ attribute: false }) accessor item!: ChatMessage; @@ -56,6 +55,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor affineFeatureFlagService!: FeatureFlagService; + @property({ attribute: false }) + accessor affineThemeService!: AppThemeService; + @property({ attribute: false }) accessor session!: CopilotChatHistoryFragment | null | undefined; @@ -68,6 +70,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor width: Signal | undefined; + @property({ attribute: false }) + accessor notificationService!: NotificationService; + get state() { const { isLast, status } = this; return isLast @@ -118,27 +123,29 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) { private renderStreamObjects(answer: StreamObject[]) { return html``; } private renderRichText(text: string) { return html``; } private renderEditorActions() { - const { item, isLast, status, host, session, docId } = this; + const { item, isLast, status, host, session } = this; if (!isChatMessage(item) || item.role !== 'assistant') return nothing; @@ -161,7 +168,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) { : EdgelessEditorActions : null; - const showActions = host && docId && !!markdown; + const showActions = host && !!markdown; return html` this.retry()} + .notificationService=${this.notificationService} > ${isLast && showActions ? html`` : nothing} `; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts index 4e0bfe60f..2a3275f6e 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/chat-panel-chips.ts @@ -2,7 +2,7 @@ import type { TagMeta } from '@affine/core/components/page-list'; import { createLitPortal } from '@blocksuite/affine/components/portal'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; -import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; +import { ShadowlessElement } from '@blocksuite/affine/std'; import { MoreVerticalIcon, PlusIcon } from '@blocksuite/icons/lit'; import { flip, offset } from '@floating-ui/dom'; import { computed, type Signal, signal } from '@preact/signals-core'; @@ -82,9 +82,6 @@ export class ChatPanelChips extends SignalWatcher( private _abortController: AbortController | null = null; - @property({ attribute: false }) - accessor host: EditorHost | null | undefined; - @property({ attribute: false }) accessor chips!: ChatChip[]; @@ -167,7 +164,6 @@ export class ChatPanelChips extends SignalWatcher( .removeChip=${this._removeChip} .checkTokenLimit=${this._checkTokenLimit} .docDisplayConfig=${this.docDisplayConfig} - .host=${this.host} >`; } if (isFileChip(chip)) { @@ -407,13 +403,8 @@ export class ChatPanelChips extends SignalWatcher( if (!contextId || !AIProvider.context) { throw new Error('Context not found'); } - if (!this.host) { - throw new Error('Host not found'); - } - const blobId = await this.host.store.blobSync.set(chip.file); const contextFile = await AIProvider.context.addContextFile(chip.file, { contextId, - blobId, }); this._updateChip(chip, { state: contextFile.status, diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/doc-chip.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/doc-chip.ts index 764e78f90..9bbdfef74 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/doc-chip.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/doc-chip.ts @@ -1,6 +1,6 @@ import track from '@affine/track'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; -import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; +import { ShadowlessElement } from '@blocksuite/affine/std'; import { Signal } from '@preact/signals-core'; import { html, type PropertyValues } from 'lit'; import { property } from 'lit/decorators.js'; @@ -36,9 +36,6 @@ export class ChatPanelDocChip extends SignalWatcher( @property({ attribute: false }) accessor docDisplayConfig!: DocDisplayConfig; - @property({ attribute: false }) - accessor host: EditorHost | null | undefined; - private chipName = new Signal(''); override connectedCallback() { @@ -103,9 +100,6 @@ export class ChatPanelDocChip extends SignalWatcher( }; private readonly processDocChip = async () => { - if (!this.host) { - return; - } try { const doc = this.docDisplayConfig.getDoc(this.chip.docId); if (!doc) { @@ -114,10 +108,7 @@ export class ChatPanelDocChip extends SignalWatcher( if (!doc.ready) { doc.load(); } - const value = await extractMarkdownFromDoc( - doc, - this.host.std.store.provider - ); + const value = await extractMarkdownFromDoc(doc); const tokenCount = estimateTokenCount(value); if (this.checkTokenLimit(this.chip, tokenCount)) { const markdown = this.chip.markdown ?? new Signal(''); diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts index 12625835e..da7183940 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-composer/ai-chat-composer.ts @@ -120,7 +120,6 @@ export class AIChatComposer extends SignalWatcher( override render() { return html` @@ -401,6 +409,8 @@ export class AIChatContent extends SignalWatcher( .isHistoryLoading=${this.isHistoryLoading} .extensions=${this.extensions} .affineFeatureFlagService=${this.affineFeatureFlagService} + .affineThemeService=${this.affineThemeService} + .notificationService=${this.notificationService} .networkSearchConfig=${this.networkSearchConfig} .reasoningConfig=${this.reasoningConfig} .width=${this.width} 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 7f7adcfef..5e596e617 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 @@ -1,8 +1,10 @@ +import type { AppThemeService } from '@affine/core/modules/theme'; import type { CopilotChatHistoryFragment } from '@affine/graphql'; import { WithDisposable } from '@blocksuite/affine/global/lit'; import { DocModeProvider, - FeatureFlagService, + type FeatureFlagService, + type NotificationService, } from '@blocksuite/affine/shared/services'; import type { EditorHost } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std'; @@ -187,6 +189,12 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor affineFeatureFlagService!: FeatureFlagService; + @property({ attribute: false }) + accessor affineThemeService!: AppThemeService; + + @property({ attribute: false }) + accessor notificationService!: NotificationService; + @property({ attribute: false }) accessor networkSearchConfig!: AINetworkSearchConfig; @@ -222,8 +230,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) { } private _renderAIOnboarding() { - return this.isHistoryLoading || - !this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding') + return this.isHistoryLoading ? nothing : html`
${repeat( @@ -311,7 +318,6 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) { } else if (isChatMessage(item) && item.role === 'assistant') { return html` this.retry()} .width=${this.width} >`; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts index d5c959aa8..c0b0b7035 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts @@ -42,7 +42,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { accessor docDisplayConfig!: DocDisplayConfig; @property({ attribute: false }) - accessor notification: NotificationService | null | undefined; + accessor notificationService!: NotificationService; @query('.history-button') accessor historyButton!: HTMLDivElement; @@ -104,21 +104,19 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { private readonly unpinConfirm = async () => { if (this.session && this.session.pinned) { try { - const confirm = this.notification - ? await this.notification.confirm({ - title: 'Switch Chat? Current chat is pinned', - message: - 'Switching will unpinned the current chat. This will change the active chat panel, allowing you to navigate between different conversation histories.', - confirmText: 'Switch Chat', - cancelText: 'Cancel', - }) - : true; + const confirm = await this.notificationService.confirm({ + title: 'Switch Chat? Current chat is pinned', + message: + 'Switching will unpinned the current chat. This will change the active chat panel, allowing you to navigate between different conversation histories.', + confirmText: 'Switch Chat', + cancelText: 'Cancel', + }); if (!confirm) { return false; } await this.onTogglePin(); } catch { - this.notification?.toast('Failed to unpin the chat'); + this.notificationService.toast('Failed to unpin the chat'); } } return true; @@ -133,7 +131,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { private readonly onSessionClick = async (sessionId: string) => { if (this.session?.sessionId === sessionId) { - this.notification?.toast('You are already in this chat'); + this.notificationService.toast('You are already in this chat'); return; } const confirm = await this.unpinConfirm(); @@ -144,7 +142,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { private readonly onDocClick = async (docId: string, sessionId: string) => { if (this.docId === docId && this.session?.sessionId === sessionId) { - this.notification?.toast('You are already in this chat'); + this.notificationService.toast('You are already in this chat'); return; } this.onOpenDoc(docId, sessionId); @@ -169,7 +167,6 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { .docDisplayConfig=${this.docDisplayConfig} .onSessionClick=${this.onSessionClick} .onDocClick=${this.onDocClick} - .notification=${this.notification} > `, portalStyles: { diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts index 9e791ef08..f96476e29 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts @@ -1,6 +1,5 @@ import type { CopilotSessionType } from '@affine/graphql'; import { WithDisposable } from '@blocksuite/affine/global/lit'; -import type { NotificationService } from '@blocksuite/affine/shared/services'; import { scrollbarStyle } from '@blocksuite/affine/shared/styles'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { ShadowlessElement } from '@blocksuite/affine/std'; @@ -134,9 +133,6 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor onDocClick!: (docId: string, sessionId: string) => void; - @property({ attribute: false }) - accessor notification: NotificationService | null | undefined; - @state() private accessor sessions: BlockSuitePresets.AIRecentSession[] = []; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts index ae868775e..c17cc0baa 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-history-clear/ai-history-clear.ts @@ -18,7 +18,7 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) { accessor session!: CopilotChatHistoryFragment | null | undefined; @property({ attribute: false }) - accessor notification: NotificationService | null | undefined; + accessor notificationService!: NotificationService; @property({ attribute: false }) accessor doc!: Store; @@ -52,15 +52,13 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) { } const sessionId = this.session.sessionId; try { - const confirm = this.notification - ? await this.notification.confirm({ - title: 'Clear History', - message: - 'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.', - confirmText: 'Confirm', - cancelText: 'Cancel', - }) - : true; + const confirm = await this.notificationService.confirm({ + title: 'Clear History', + message: + 'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.', + confirmText: 'Confirm', + cancelText: 'Cancel', + }); if (confirm) { const actionIds = this.chatContextValue.messages @@ -71,11 +69,11 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) { this.doc.id, [...(sessionId ? [sessionId] : []), ...(actionIds || [])] ); - this.notification?.toast('History cleared'); + this.notificationService.toast('History cleared'); this.onHistoryCleared?.(); } } catch { - this.notification?.toast('Failed to clear history'); + this.notificationService.toast('Failed to clear history'); } }; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts index cd2403c62..11197a601 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts @@ -1,17 +1,15 @@ import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { WithDisposable } from '@blocksuite/affine/global/lit'; -import type { EditorHost } from '@blocksuite/affine/std'; +import type { ColorScheme } from '@blocksuite/affine/model'; import { ShadowlessElement } from '@blocksuite/affine/std'; import type { ExtensionType } from '@blocksuite/affine/store'; +import type { Signal } from '@preact/signals-core'; import { html } from 'lit'; import { property } from 'lit/decorators.js'; import { createTextRenderer } from '../../components/text-renderer'; export class ChatContentRichText extends WithDisposable(ShadowlessElement) { - @property({ attribute: false }) - accessor host: EditorHost | null | undefined; - @property({ attribute: false }) accessor text!: string; @@ -24,12 +22,16 @@ export class ChatContentRichText extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor affineFeatureFlagService!: FeatureFlagService; + @property({ attribute: false }) + accessor theme!: Signal; + protected override render() { - const { text, host } = this; - return html`${createTextRenderer(host, { + const { text } = this; + return html`${createTextRenderer({ customHeading: true, extensions: this.extensions, affineFeatureFlagService: this.affineFeatureFlagService, + theme: this.theme, })(text, this.state)}`; } } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts index f7a8e0142..039afb126 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts @@ -1,14 +1,13 @@ import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { WithDisposable } from '@blocksuite/affine/global/lit'; -import { ImageProxyService } from '@blocksuite/affine/shared/adapters'; -import type { EditorHost } from '@blocksuite/affine/std'; -import { ShadowlessElement } from '@blocksuite/affine/std'; +import type { ColorScheme } from '@blocksuite/affine/model'; +import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; import type { ExtensionType } from '@blocksuite/affine/store'; +import type { NotificationService } from '@blocksuite/affine-shared/services'; import type { Signal } from '@preact/signals-core'; import { css, html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; -import { BlockDiffProvider } from '../../services/block-diff'; import type { AffineAIPanelState } from '../../widgets/ai-panel/type'; import type { StreamObject } from '../ai-chat-messages'; @@ -42,12 +41,16 @@ export class ChatContentStreamObjects extends WithDisposable( @property({ attribute: false }) accessor affineFeatureFlagService!: FeatureFlagService; + @property({ attribute: false }) + accessor theme!: Signal; + + @property({ attribute: false }) + accessor notificationService!: NotificationService; + private renderToolCall(streamObject: StreamObject) { if (streamObject.type !== 'tool-call') { return nothing; } - const imageProxyService = this.host?.store.get(ImageProxyService); - const blockDiffService = this.host?.view.std.getOptional(BlockDiffProvider); switch (streamObject.toolName) { case 'web_crawl_exa': @@ -55,7 +58,6 @@ export class ChatContentStreamObjects extends WithDisposable( `; case 'web_search_exa': @@ -63,7 +65,6 @@ export class ChatContentStreamObjects extends WithDisposable( `; case 'doc_compose': @@ -72,7 +73,8 @@ export class ChatContentStreamObjects extends WithDisposable( .std=${this.host?.std} .data=${streamObject} .width=${this.width} - .imageProxyService=${imageProxyService} + .theme=${this.theme} + .notificationService=${this.notificationService} > `; case 'code_artifact': @@ -81,7 +83,6 @@ export class ChatContentStreamObjects extends WithDisposable( .std=${this.host?.std} .data=${streamObject} .width=${this.width} - .imageProxyService=${imageProxyService} > `; case 'doc_edit': @@ -89,7 +90,7 @@ export class ChatContentStreamObjects extends WithDisposable( `; default: { @@ -105,8 +106,6 @@ export class ChatContentStreamObjects extends WithDisposable( if (streamObject.type !== 'tool-result') { return nothing; } - const imageProxyService = this.host?.store.get(ImageProxyService); - const blockDiffService = this.host?.view.std.getOptional(BlockDiffProvider); switch (streamObject.toolName) { case 'web_crawl_exa': @@ -114,7 +113,6 @@ export class ChatContentStreamObjects extends WithDisposable( `; case 'web_search_exa': @@ -122,7 +120,6 @@ export class ChatContentStreamObjects extends WithDisposable( `; case 'doc_compose': @@ -131,7 +128,8 @@ export class ChatContentStreamObjects extends WithDisposable( .std=${this.host?.std} .data=${streamObject} .width=${this.width} - .imageProxyService=${imageProxyService} + .theme=${this.theme} + .notificationService=${this.notificationService} > `; case 'code_artifact': @@ -140,7 +138,6 @@ export class ChatContentStreamObjects extends WithDisposable( .std=${this.host?.std} .data=${streamObject} .width=${this.width} - .imageProxyService=${imageProxyService} > `; case 'doc_edit': @@ -148,8 +145,8 @@ export class ChatContentStreamObjects extends WithDisposable( `; default: { @@ -158,7 +155,6 @@ export class ChatContentStreamObjects extends WithDisposable( `; } @@ -167,11 +163,11 @@ export class ChatContentStreamObjects extends WithDisposable( private renderRichText(text: string) { return html``; } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts index 743e5f155..77434ec70 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-scrollable-text-renderer.ts @@ -62,7 +62,7 @@ export class AIScrollableTextRenderer extends WithDisposable( } override render() { - const { host, answer, state, textRendererOptions } = this; + const { answer, state, textRendererOptions } = this; return html`
| undefined; - @property({ attribute: false }) - accessor imageProxyService: ImageProxyService | null | undefined; - @property({ attribute: false }) accessor std: BlockStdScope | undefined; diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts index 16c4480de..1bee79756 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-compose.ts @@ -5,14 +5,11 @@ import { LoadingIcon } from '@blocksuite/affine/components/icons'; import { toast } from '@blocksuite/affine/components/toast'; import { WithDisposable } from '@blocksuite/affine/global/lit'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; -import type { ImageProxyService } from '@blocksuite/affine/shared/adapters'; -import { - NotificationProvider, - ThemeProvider, -} from '@blocksuite/affine/shared/services'; +import type { ColorScheme } from '@blocksuite/affine/model'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { type BlockStdScope, ShadowlessElement } from '@blocksuite/affine/std'; import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc'; +import type { NotificationService } from '@blocksuite/affine-shared/services'; import { CopyIcon, PageIcon, ToolIcon } from '@blocksuite/icons/lit'; import { type Signal } from '@preact/signals-core'; import { css, html, nothing, type PropertyValues } from 'lit'; @@ -110,10 +107,13 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { accessor width: Signal | undefined; @property({ attribute: false }) - accessor imageProxyService: ImageProxyService | null | undefined; + accessor std: BlockStdScope | undefined; @property({ attribute: false }) - accessor std: BlockStdScope | undefined; + accessor notificationService!: NotificationService; + + @property({ attribute: false }) + accessor theme!: Signal; override updated(changedProperties: PropertyValues) { super.updated(changedProperties); @@ -147,7 +147,6 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { return; } const workspace = std.store.workspace; - const notificationService = std.get(NotificationProvider); const refNodeSlots = std.getOptional(RefNodeSlotsProvider); const docId = await MarkdownTransformer.importMarkdownToDoc({ collection: workspace, @@ -157,7 +156,7 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { extensions: getStoreManager().config.init().value.get('store'), }); if (docId) { - const open = await notificationService.confirm({ + const open = await this.notificationService.confirm({ title: 'Open the doc you just created', message: 'Doc saved successfully! Would you like to open it now?', cancelText: 'Cancel', @@ -200,8 +199,6 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) { ${successResult ? html``; } - const theme = this.std.get(ThemeProvider).theme; - const { LinkedDocEmptyBanner } = getEmbedLinkedDocIcons( - theme, + this.theme.value, 'page', 'horizontal' ); diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-edit.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-edit.ts index 16394eb2d..c49a0b14d 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-edit.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-edit.ts @@ -1,7 +1,7 @@ import { WithDisposable } from '@blocksuite/affine/global/lit'; -import { NotificationProvider } from '@blocksuite/affine/shared/services'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; +import type { NotificationService } from '@blocksuite/affine-shared/services'; import { CloseIcon, CopyIcon, @@ -14,7 +14,7 @@ import { import { css, html, nothing } from 'lit'; import { property, state } from 'lit/decorators.js'; -import type { BlockDiffService } from '../../services/block-diff'; +import { BlockDiffProvider } from '../../services/block-diff'; import { diffMarkdown } from '../../utils/apply-model/markdown-diff'; import { copyText } from '../../utils/editor-actions'; import type { ToolError } from './type'; @@ -190,14 +190,18 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) { accessor data!: DocEditToolCall | DocEditToolResult; @property({ attribute: false }) - accessor blockDiffService: BlockDiffService | undefined; + accessor renderRichText!: (text: string) => string; @property({ attribute: false }) - accessor renderRichText!: (text: string) => string; + accessor notificationService!: NotificationService; @state() accessor isCollapsed = false; + get blockDiffService() { + return this.host?.std.getOptional(BlockDiffProvider); + } + private async _handleApply(markdown: string) { if (!this.host) { return; @@ -229,14 +233,9 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) { if (!this.host) { return; } - const success = await copyText( - this.host, - removeMarkdownComments(changedMarkdown) - ); + const success = await copyText(removeMarkdownComments(changedMarkdown)); if (success) { - const notificationService = - this.host?.std.getOptional(NotificationProvider); - notificationService?.notify({ + this.notificationService.notify({ title: 'Copied to clipboard', accent: 'success', onClose: function (): void {}, diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts index 088f878b5..ff71d4e77 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts @@ -1,7 +1,7 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; -import { type ImageProxyService } from '@blocksuite/affine/shared/adapters'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { ShadowlessElement } from '@blocksuite/affine/std'; +import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts'; import { ToggleDownIcon, ToolIcon } from '@blocksuite/icons/lit'; import { type Signal } from '@preact/signals-core'; import { css, html, nothing, type TemplateResult } from 'lit'; @@ -205,12 +205,11 @@ export class ToolResultCard extends SignalWatcher( @property({ attribute: false }) accessor width: Signal | undefined; - @property({ attribute: false }) - accessor imageProxyService: ImageProxyService | null | undefined; - @state() private accessor isCollapsed = true; + private readonly imageProxyURL = DEFAULT_IMAGE_PROXY_ENDPOINT; + protected override render() { return html`
@@ -272,15 +271,20 @@ export class ToolResultCard extends SignalWatcher( `; } + buildUrl(imageUrl: string) { + if (imageUrl.startsWith(this.imageProxyURL)) { + return imageUrl; + } + return `${this.imageProxyURL}?url=${encodeURIComponent(imageUrl)}`; + } + private renderIcon(icon: string | TemplateResult<1> | undefined) { if (!icon) { return nothing; } + if (typeof icon === 'string') { - if (this.imageProxyService) { - return html``; - } - return html``; + return html``; } return html`${icon}`; } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts index 93e62d531..18b5ee9fe 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts @@ -1,5 +1,4 @@ import { WithDisposable } from '@blocksuite/affine/global/lit'; -import type { ImageProxyService } from '@blocksuite/affine/shared/adapters'; import { ShadowlessElement } from '@blocksuite/affine/std'; import { WebIcon } from '@blocksuite/icons/lit'; import type { Signal } from '@preact/signals-core'; @@ -40,9 +39,6 @@ export class WebCrawlTool extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor width: Signal | undefined; - @property({ attribute: false }) - accessor imageProxyService: ImageProxyService | null | undefined; - renderToolCall() { return html` `; } diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts index 184f768b9..f59af6642 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts @@ -1,5 +1,4 @@ import { WithDisposable } from '@blocksuite/affine/global/lit'; -import type { ImageProxyService } from '@blocksuite/affine/shared/adapters'; import { ShadowlessElement } from '@blocksuite/affine/std'; import { WebIcon } from '@blocksuite/icons/lit'; import type { Signal } from '@preact/signals-core'; @@ -40,9 +39,6 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor width: Signal | undefined; - @property({ attribute: false }) - accessor imageProxyService: ImageProxyService | null | undefined; - renderToolCall() { return html` `; } diff --git a/packages/frontend/core/src/blocksuite/ai/components/chat-action-list.ts b/packages/frontend/core/src/blocksuite/ai/components/chat-action-list.ts index 14233e649..21bcaabe5 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/chat-action-list.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/chat-action-list.ts @@ -1,11 +1,11 @@ import type { CopilotChatHistoryFragment } from '@affine/graphql'; import type { ImageSelection } from '@blocksuite/affine/shared/selection'; -import { NotificationProvider } from '@blocksuite/affine/shared/services'; import type { BlockSelection, EditorHost, TextSelection, } from '@blocksuite/affine/std'; +import type { NotificationService } from '@blocksuite/affine-shared/services'; import { css, html, LitElement, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -96,6 +96,9 @@ export class ChatActionList extends LitElement { @property({ attribute: 'data-testid', reflect: true }) accessor testId = 'chat-action-list'; + @property({ attribute: false }) + accessor notificationService!: NotificationService; + override render() { const { actions } = this; if (!actions.length) { @@ -148,7 +151,7 @@ export class ChatActionList extends LitElement { messageId ); if (success) { - this.host.std.getOptional(NotificationProvider)?.notify({ + this.notificationService.notify({ title: action.toast, accent: 'success', onClose: function (): void {}, diff --git a/packages/frontend/core/src/blocksuite/ai/components/copy-more.ts b/packages/frontend/core/src/blocksuite/ai/components/copy-more.ts index 7e8ddb0a5..8dcb39268 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/copy-more.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/copy-more.ts @@ -2,7 +2,6 @@ import type { CopilotChatHistoryFragment } from '@affine/graphql'; import { Tooltip } from '@blocksuite/affine/components/toolbar'; import { WithDisposable } from '@blocksuite/affine/global/lit'; import { noop } from '@blocksuite/affine/global/utils'; -import { NotificationProvider } from '@blocksuite/affine/shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { createButtonPopper } from '@blocksuite/affine/shared/utils'; import type { @@ -10,6 +9,7 @@ import type { EditorHost, TextSelection, } from '@blocksuite/affine/std'; +import type { NotificationService } from '@blocksuite/affine-shared/services'; import { CopyIcon, MoreHorizontalIcon, ResetIcon } from '@blocksuite/icons/lit'; import { css, html, LitElement, nothing, type PropertyValues } from 'lit'; import { property, query, state } from 'lit/decorators.js'; @@ -131,14 +131,15 @@ export class ChatCopyMore extends WithDisposable(LitElement) { @property({ attribute: 'data-testid', reflect: true }) accessor testId = 'chat-actions'; + @property({ attribute: false }) + accessor notificationService!: NotificationService; + private _toggle() { this._morePopper?.toggle(); } private readonly _notifySuccess = (title: string) => { - const notificationService = - this.host?.std.getOptional(NotificationProvider); - notificationService?.notify({ + this.notificationService.notify({ title: title, accent: 'success', onClose: function (): void {}, @@ -165,7 +166,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) { override render() { const { host, content, isLast, messageId, actions } = this; - const showMoreIcon = !isLast && host && actions.length > 0; + const showMoreIcon = !isLast && actions.length > 0; return html`
- ${content && host + ${content ? html`
{ - const success = await copyText(host, content); + const success = await copyText(content); if (success) { this._notifySuccess('Copied to clipboard'); } @@ -201,7 +202,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) { Retry
` : nothing} - ${showMoreIcon + ${showMoreIcon && host ? html`
Promise; @@ -177,6 +186,17 @@ export class PlaygroundChat extends SignalWatcher( // request counter to track the latest request private _updateHistoryCounter = 0; + get messages() { + return this.chatContextValue.messages.filter(item => { + return ( + isChatMessage(item) || + item.messages?.length === 3 || + (HISTORY_IMAGE_ACTIONS.includes(item.action) && + item.messages?.length === 2) + ); + }); + } + private readonly _initPanel = async () => { const userId = (await AIProvider.userInfo)?.id; if (!userId) return; @@ -276,7 +296,6 @@ export class PlaygroundChat extends SignalWatcher( override render() { const [done, total] = this.embeddingProgress; const isEmbedding = total > 0 && done < total; - const notification = this.host.std.getOptional(NotificationProvider); return html`
@@ -294,7 +313,7 @@ export class PlaygroundChat extends SignalWatcher( @@ -312,8 +331,11 @@ export class PlaygroundChat extends SignalWatcher( .updateContext=${this.updateContext} .extensions=${this.extensions} .affineFeatureFlagService=${this.affineFeatureFlagService} + .affineThemeService=${this.affineThemeService} + .notificationService=${this.notificationService} .networkSearchConfig=${this.networkSearchConfig} .reasoningConfig=${this.reasoningConfig} + .messages=${this.messages} >
diff --git a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts index 3ec05e1d8..22a78b011 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts @@ -1,17 +1,15 @@ import { createReactComponentFromLit } from '@affine/component'; -import { getStoreManager } from '@affine/core/blocksuite/manager/store'; import { getViewManager } from '@affine/core/blocksuite/manager/view'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { PeekViewProvider } from '@blocksuite/affine/components/peek'; -import { Container, type ServiceProvider } from '@blocksuite/affine/global/di'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; +import type { ColorScheme } from '@blocksuite/affine/model'; import { codeBlockWrapMiddleware, defaultImageProxyMiddleware, ImageProxyService, } from '@blocksuite/affine/shared/adapters'; -import { ThemeProvider } from '@blocksuite/affine/shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { BlockStdScope, @@ -22,10 +20,10 @@ import { import type { ExtensionType, Query, - Schema, Store, TransformerMiddleware, } from '@blocksuite/affine/store'; +import type { Signal } from '@preact/signals-core'; import { darkCssVariablesV2, lightCssVariablesV2, @@ -103,6 +101,7 @@ export type TextRendererOptions = { additionalMiddlewares?: TransformerMiddleware[]; testId?: string; affineFeatureFlagService?: FeatureFlagService; + theme?: Signal; }; // todo: refactor it for more general purpose usage instead of AI only? @@ -221,6 +220,8 @@ export class TextRenderer extends SignalWatcher( private _doc: Store | null = null; + private _host: EditorHost | null = null; + private readonly _query: Query = { mode: 'strict', match: [ @@ -243,7 +244,7 @@ export class TextRenderer extends SignalWatcher( private _timer?: ReturnType | null = null; private readonly _subscribeDocLinkClicked = () => { - const refNodeSlots = this.host?.std.getOptional(RefNodeSlotsProvider); + const refNodeSlots = this._host?.std.getOptional(RefNodeSlotsProvider); if (!refNodeSlots) return; this.disposables.add( refNodeSlots.docLinkClicked @@ -254,7 +255,7 @@ export class TextRenderer extends SignalWatcher( ) .subscribe(options => { // Open the doc in center peek - this.host?.std + this._host?.std .getOptional(PeekViewProvider) ?.peek({ docId: options.pageId, @@ -268,40 +269,27 @@ export class TextRenderer extends SignalWatcher( if (this._answers.length > 0) { const latestAnswer = this._answers.pop(); this._answers = []; - const schema = this.schema ?? this.host?.std.store.schema; - let provider: ServiceProvider; - if (this.host) { - provider = this.host.std.store.provider; - } else { - const container = new Container(); - getStoreManager() - .config.init() - .value.get('store') - .forEach(ext => { - ext.setup(container); - }); - - provider = container.provider(); - } - if (latestAnswer && schema) { + if (latestAnswer) { const middlewares = [ defaultImageProxyMiddleware, codeBlockWrapMiddleware(true), ...(this.options.additionalMiddlewares ?? []), ]; - const affineFeatureFlagService = this.options.affineFeatureFlagService; markDownToDoc( - provider, - schema, latestAnswer, middlewares, - affineFeatureFlagService + this.options.affineFeatureFlagService ) .then(doc => { this.disposeDoc(); this._doc = doc.doc.getStore({ query: this._query, }); + this._host = new BlockStdScope({ + store: this._doc, + extensions: + this.options.extensions ?? getCustomPageEditorBlockSpecs(), + }).render(); this.disposables.add(() => { doc.doc.removeStore({ query: this._query }); }); @@ -309,14 +297,10 @@ export class TextRenderer extends SignalWatcher( this.requestUpdate(); if (this.state !== 'generating') { this._doc.load(); - // LinkPreviewService & ImageProxyService config should read from host settings - const imageProxyService = - this.host?.std.store.get(ImageProxyService); - if (imageProxyService) { - this._doc - ?.get(ImageProxyService) - .setImageProxyURL(imageProxyService.imageProxyURL); - } + const imageProxyService = this._host.std.get(ImageProxyService); + imageProxyService.setImageProxyURL( + imageProxyService.imageProxyURL + ); this._clearTimer(); } }) @@ -341,7 +325,6 @@ export class TextRenderer extends SignalWatcher( private disposeDoc() { this._doc?.dispose(); - this._doc?.workspace.dispose(); } override disconnectedCallback() { @@ -355,22 +338,22 @@ export class TextRenderer extends SignalWatcher( return nothing; } - const { customHeading, testId } = this.options; + const { customHeading, testId = 'ai-text-renderer' } = this.options; const classes = classMap({ 'text-renderer-container': true, 'custom-heading': !!customHeading, }); - const theme = this.host?.std.get(ThemeProvider).app$.value; + const theme = this.options.theme?.value; return html` -
+
${keyed( this._doc, html`
- ${new BlockStdScope({ - store: this._doc, - extensions: - this.options.extensions ?? getCustomPageEditorBlockSpecs(), - }).render()} + ${this._host}
` )}
@@ -416,12 +399,6 @@ export class TextRenderer extends SignalWatcher( @property({ attribute: false }) accessor answer!: string; - @property({ attribute: false }) - accessor host: EditorHost | null | undefined; - - @property({ attribute: false }) - accessor schema: Schema | null = null; - @property({ attribute: false }) accessor options!: TextRendererOptions; @@ -429,14 +406,10 @@ export class TextRenderer extends SignalWatcher( accessor state: AffineAIPanelState | undefined = undefined; } -export const createTextRenderer = ( - host: EditorHost | null | undefined, - options: TextRendererOptions -) => { +export const createTextRenderer = (options: TextRendererOptions) => { return (answer: string, state?: AffineAIPanelState) => { return html``; } + const notificationService = this.host.std.get(NotificationProvider); + return html`
this.retry()} + .notificationService=${notificationService} >` : nothing} ${shouldRenderActions @@ -495,6 +498,7 @@ export class AIChatBlockPeekView extends LitElement { .content=${markdown} .messageId=${message.id ?? undefined} .layoutDirection=${'horizontal'} + .notificationService=${notificationService} >` : nothing}
`; @@ -569,7 +573,7 @@ export class AIChatBlockPeekView extends LitElement { } = this; const { messages: currentChatMessages } = chatContext; - const notification = this.host.std.getOptional(NotificationProvider); + const notificationService = this.host.std.get(NotificationProvider); return html`
@@ -578,7 +582,7 @@ export class AIChatBlockPeekView extends LitElement { .session=${this.forkSession} .onHistoryCleared=${this._onHistoryCleared} .chatContextValue=${chatContext} - .notification=${notification} + .notificationService=${notificationService} >
diff --git a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx index 8b6713ef1..b40ee67e0 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx +++ b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx @@ -2,6 +2,7 @@ import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onb import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud'; import type { GlobalDialogService } from '@affine/core/modules/dialogs'; import { + type AddContextFileInput, ContextCategories, type ContextWorkspaceEmbeddingStatus, type getCopilotHistoriesQuery, @@ -609,10 +610,7 @@ Could you make a new website based on these notes and send back just the html fi removeContextDoc: async (options: { contextId: string; docId: string }) => { return client.removeContextDoc(options); }, - addContextFile: async ( - file: File, - options: { contextId: string; blobId: string } - ) => { + addContextFile: async (file: File, options: AddContextFileInput) => { return client.addContextFile(file, options); }, removeContextFile: async (options: { diff --git a/packages/frontend/core/src/blocksuite/ai/utils/editor-actions.ts b/packages/frontend/core/src/blocksuite/ai/utils/editor-actions.ts index dddeb5617..64561532f 100644 --- a/packages/frontend/core/src/blocksuite/ai/utils/editor-actions.ts +++ b/packages/frontend/core/src/blocksuite/ai/utils/editor-actions.ts @@ -1,10 +1,13 @@ import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace'; +import { clipboardConfigs } from '@blocksuite/affine/foundation/clipboard'; import { defaultImageProxyMiddleware } from '@blocksuite/affine/shared/adapters'; import { replaceSelectedTextWithBlocksCommand } from '@blocksuite/affine/shared/commands'; import { isInsideEdgelessEditor } from '@blocksuite/affine/shared/utils'; import { type BlockComponent, BlockSelection, + BlockStdScope, + Clipboard, type EditorHost, SurfaceSelection, type TextSelection, @@ -185,27 +188,25 @@ export const replace = async ( }; export const copyTextAnswer = async (panel: AffineAIPanelWidget) => { - const host = panel.host; if (!panel.answer) { return false; } - return copyText(host, panel.answer); + return copyText(panel.answer); }; -export const copyText = async (host: EditorHost, text: string) => { - const previewDoc = await markDownToDoc( - host.std.store.provider, - host.std.store.schema, - text, - [defaultImageProxyMiddleware] - ); +export const copyText = async (text: string) => { + const previewDoc = await markDownToDoc(text, [defaultImageProxyMiddleware]); const models = previewDoc .getBlocksByFlavour('affine:note') .map(b => b.model) .flatMap(model => model.children); const slice = Slice.fromModels(previewDoc, models); - await host.std.clipboard.copySlice(slice); + const std = new BlockStdScope({ + store: previewDoc, + extensions: [...clipboardConfigs], + }); + const clipboard = std.provider.get(Clipboard); + await clipboard.copySlice(slice); previewDoc.dispose(); - previewDoc.workspace.dispose(); return true; }; diff --git a/packages/frontend/core/src/blocksuite/ai/utils/extract.ts b/packages/frontend/core/src/blocksuite/ai/utils/extract.ts index b41cd1c97..bb9e19897 100644 --- a/packages/frontend/core/src/blocksuite/ai/utils/extract.ts +++ b/packages/frontend/core/src/blocksuite/ai/utils/extract.ts @@ -1,4 +1,3 @@ -import type { ServiceProvider } from '@blocksuite/affine/global/di'; import { DatabaseBlockModel, ImageBlockModel, @@ -19,10 +18,11 @@ import { isInsideEdgelessEditor, matchModels, } from '@blocksuite/affine/shared/utils'; -import type { EditorHost } from '@blocksuite/affine/std'; +import { BlockStdScope, type EditorHost } from '@blocksuite/affine/std'; import type { BlockModel, Store } from '@blocksuite/affine/store'; import { Slice, toDraftModel } from '@blocksuite/affine/store'; +import { getStoreManager } from '../../manager/store'; import type { ChatContextValue } from '../components/ai-chat-content'; import { getSelectedImagesAsBlobs, @@ -96,12 +96,13 @@ async function extractPageSelected( } } -export async function extractMarkdownFromDoc( - doc: Store, - provider: ServiceProvider -): Promise { +export async function extractMarkdownFromDoc(doc: Store): Promise { + const std = new BlockStdScope({ + store: doc, + extensions: getStoreManager().config.init().value.get('store'), + }); const transformer = await getTransformer(doc); - const adapter = new MarkdownAdapter(transformer, provider); + const adapter = new MarkdownAdapter(transformer, std.provider); const blockModels = getNoteBlockModels(doc); const textModels = blockModels.filter( model => !matchModels(model, [ImageBlockModel, DatabaseBlockModel]) diff --git a/packages/frontend/core/src/blocksuite/ai/widgets/block-diff/block.ts b/packages/frontend/core/src/blocksuite/ai/widgets/block-diff/block.ts index d14d753b3..c0987028c 100644 --- a/packages/frontend/core/src/blocksuite/ai/widgets/block-diff/block.ts +++ b/packages/frontend/core/src/blocksuite/ai/widgets/block-diff/block.ts @@ -1,4 +1,5 @@ import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std'; +import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { css, html, nothing, type TemplateResult } from 'lit'; import { literal, unsafeStatic } from 'lit/static-html.js'; @@ -98,9 +99,9 @@ export class AffineBlockDiffWidgetForBlock extends WidgetComponent { : html`
)} diff --git a/packages/frontend/core/src/blocksuite/utils/markdown-utils.ts b/packages/frontend/core/src/blocksuite/utils/markdown-utils.ts index d4bed0972..df4de056f 100644 --- a/packages/frontend/core/src/blocksuite/utils/markdown-utils.ts +++ b/packages/frontend/core/src/blocksuite/utils/markdown-utils.ts @@ -1,6 +1,5 @@ import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace'; -import type { ServiceProvider } from '@blocksuite/affine/global/di'; import { defaultImageProxyMiddleware, embedSyncedDocMiddleware, @@ -11,6 +10,7 @@ import { titleMiddleware, } from '@blocksuite/affine/shared/adapters'; import { + BlockStdScope, type EditorHost, type TextRangePoint, TextSelection, @@ -19,7 +19,6 @@ import type { BlockModel, BlockSnapshot, DraftModel, - Schema, Slice, SliceSnapshot, Store, @@ -27,6 +26,43 @@ import type { } from '@blocksuite/affine/store'; import { toDraftModel, Transformer } from '@blocksuite/affine/store'; import { Doc as YDoc } from 'yjs'; + +import { getStoreManager } from '../manager/store'; + +interface MarkdownWorkspace { + collection: WorkspaceImpl; + std: BlockStdScope; +} + +let markdownWorkspace: MarkdownWorkspace | null = null; + +const getMarkdownWorkspace = ( + featureFlagService?: FeatureFlagService +): MarkdownWorkspace => { + if (markdownWorkspace) { + return markdownWorkspace; + } + + const collection = new WorkspaceImpl({ + rootDoc: new YDoc({ guid: 'markdownToDoc' }), + featureFlagService: featureFlagService, + }); + collection.meta.initialize(); + + const mockDoc = collection.createDoc('mock-id'); + const std = new BlockStdScope({ + store: mockDoc.getStore(), + extensions: getStoreManager().config.init().value.get('store'), + }); + + markdownWorkspace = { + collection, + std, + }; + + return markdownWorkspace; +}; + const updateSnapshotText = ( point: TextRangePoint, snapshot: BlockSnapshot, @@ -184,20 +220,14 @@ export async function replaceFromMarkdown( } export async function markDownToDoc( - provider: ServiceProvider, - schema: Schema, answer: string, middlewares?: TransformerMiddleware[], affineFeatureFlagService?: FeatureFlagService ) { - // Should not create a new doc in the original collection - const collection = new WorkspaceImpl({ - rootDoc: new YDoc({ guid: 'markdownToDoc' }), - featureFlagService: affineFeatureFlagService, - }); - collection.meta.initialize(); + const { collection, std } = getMarkdownWorkspace(affineFeatureFlagService); + const transformer = new Transformer({ - schema, + schema: std.store.schema, blobCRUD: collection.blobSync, docCRUD: { create: (id: string) => collection.createDoc(id).getStore({ id }), @@ -206,7 +236,7 @@ export async function markDownToDoc( }, middlewares, }); - const mdAdapter = new MarkdownAdapter(transformer, provider); + const mdAdapter = new MarkdownAdapter(transformer, std.store.provider); const doc = await mdAdapter.toDoc({ file: answer, assets: transformer.assetsManager, diff --git a/packages/frontend/core/src/blocksuite/view-extensions/editor-view/notification-service.tsx b/packages/frontend/core/src/blocksuite/view-extensions/editor-view/notification-service.tsx index e552c9d6a..fcf5f8562 100644 --- a/packages/frontend/core/src/blocksuite/view-extensions/editor-view/notification-service.tsx +++ b/packages/frontend/core/src/blocksuite/view-extensions/editor-view/notification-service.tsx @@ -1,4 +1,5 @@ import { + type ConfirmModalProps, Input, type Notification, notify, @@ -7,127 +8,156 @@ import { toReactNode, type useConfirmModal, } from '@affine/component'; -import { NotificationExtension } from '@blocksuite/affine/shared/services'; +import { + NotificationExtension, + type NotificationService, +} from '@blocksuite/affine/shared/services'; + +export class NotificationServiceImpl implements NotificationService { + constructor( + private readonly closeConfirmModal: () => void, + private readonly openConfirmModal: (props: ConfirmModalProps) => void + ) {} + + confirm = async ({ + title, + message, + confirmText, + cancelText, + abort, + }: Parameters[0]) => { + return new Promise(resolve => { + this.openConfirmModal({ + title: toReactNode(title), + description: toReactNode(message), + confirmText, + confirmButtonOptions: { + variant: 'primary', + }, + cancelText, + onConfirm: () => { + resolve(true); + }, + onCancel: () => { + resolve(false); + }, + }); + abort?.addEventListener('abort', () => { + resolve(false); + this.closeConfirmModal(); + }); + }); + }; + + prompt = async ({ + title, + message, + confirmText, + placeholder, + cancelText, + autofill, + abort, + }: Parameters[0]) => { + return new Promise(resolve => { + let value = autofill || ''; + const description = ( +
+ {toReactNode(message)} + (value = e)} + /> +
+ ); + this.openConfirmModal({ + title: toReactNode(title), + description: description, + confirmText: confirmText ?? 'Confirm', + confirmButtonOptions: { + variant: 'primary', + }, + cancelText: cancelText ?? 'Cancel', + onConfirm: () => { + resolve(value); + }, + onCancel: () => { + resolve(null); + }, + autoFocusConfirm: false, + }); + abort?.addEventListener('abort', () => { + resolve(null); + this.closeConfirmModal(); + }); + }); + }; + + toast = (message: string, options: ToastOptions) => { + return toast(message, options); + }; + + notify = (notification: Parameters[0]) => { + const accentToNotify = { + error: notify.error, + success: notify.success, + warning: notify.warning, + info: notify, + }; + + const fn = accentToNotify[notification.accent || 'info']; + if (!fn) { + throw new Error('Invalid notification accent'); + } + + const toAffineNotificationActions = ( + actions: (typeof notification)['actions'] + ): Notification['actions'] => { + if (!actions) return undefined; + + return actions.map(({ label, onClick, key }) => { + return { + key, + label: toReactNode(label), + onClick, + }; + }); + }; + + const toastId = fn( + { + title: toReactNode(notification.title), + message: toReactNode(notification.message), + actions: toAffineNotificationActions(notification.actions), + onDismiss: notification.onClose, + }, + { + duration: notification.duration || 0, + onDismiss: notification.onClose, + onAutoClose: notification.onClose, + } + ); + + notification.abort?.addEventListener('abort', () => { + notify.dismiss(toastId); + }); + }; + + notifyWithUndoAction = ( + options: Parameters[0] + ) => { + this.notify(options); + }; +} export function patchNotificationService({ closeConfirmModal, openConfirmModal, }: ReturnType) { - return NotificationExtension({ - confirm: async ({ title, message, confirmText, cancelText, abort }) => { - return new Promise(resolve => { - openConfirmModal({ - title: toReactNode(title), - description: toReactNode(message), - confirmText, - confirmButtonOptions: { - variant: 'primary', - }, - cancelText, - onConfirm: () => { - resolve(true); - }, - onCancel: () => { - resolve(false); - }, - }); - abort?.addEventListener('abort', () => { - resolve(false); - closeConfirmModal(); - }); - }); - }, - prompt: async ({ - title, - message, - confirmText, - placeholder, - cancelText, - autofill, - abort, - }) => { - return new Promise(resolve => { - let value = autofill || ''; - const description = ( -
- {toReactNode(message)} - (value = e)} - /> -
- ); - openConfirmModal({ - title: toReactNode(title), - description: description, - confirmText: confirmText ?? 'Confirm', - confirmButtonOptions: { - variant: 'primary', - }, - cancelText: cancelText ?? 'Cancel', - onConfirm: () => { - resolve(value); - }, - onCancel: () => { - resolve(null); - }, - autoFocusConfirm: false, - }); - abort?.addEventListener('abort', () => { - resolve(null); - closeConfirmModal(); - }); - }); - }, - toast: (message: string, options: ToastOptions) => { - return toast(message, options); - }, - notify: notification => { - const accentToNotify = { - error: notify.error, - success: notify.success, - warning: notify.warning, - info: notify, - }; - - const fn = accentToNotify[notification.accent || 'info']; - if (!fn) { - throw new Error('Invalid notification accent'); - } - - const toAffineNotificationActions = ( - actions: (typeof notification)['actions'] - ): Notification['actions'] => { - if (!actions) return undefined; - - return actions.map(({ label, onClick, key }) => { - return { - key, - label: toReactNode(label), - onClick, - }; - }); - }; - - const toastId = fn( - { - title: toReactNode(notification.title), - message: toReactNode(notification.message), - actions: toAffineNotificationActions(notification.actions), - onDismiss: notification.onClose, - }, - { - duration: notification.duration || 0, - onDismiss: notification.onClose, - onAutoClose: notification.onClose, - } - ); - - notification.abort?.addEventListener('abort', () => { - notify.dismiss(toastId); - }); - }, - }); + const notificationService = new NotificationServiceImpl( + closeConfirmModal, + openConfirmModal + ); + return NotificationExtension(notificationService); } 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 c10c97c31..f40516bef 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx @@ -1,11 +1,10 @@ -import { observeResize } from '@affine/component'; +import { observeResize, useConfirmModal } from '@affine/component'; import { CopilotClient } from '@affine/core/blocksuite/ai'; import { AIChatContent } from '@affine/core/blocksuite/ai/components/ai-chat-content'; import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar'; -import { getCustomPageEditorBlockSpecs } from '@affine/core/blocksuite/ai/components/text-renderer'; import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt'; +import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service'; import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config'; -import { getCollection } from '@affine/core/desktop/dialogs/setting/general-setting/editor/edgeless/docs'; import { EventSourceService, FetchService, @@ -13,6 +12,7 @@ import { } from '@affine/core/modules/cloud'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { AppThemeService } from '@affine/core/modules/theme'; import { ViewBody, ViewHeader, @@ -21,8 +21,6 @@ import { } from '@affine/core/modules/workbench'; import { WorkspaceService } from '@affine/core/modules/workspace'; import { useI18n } from '@affine/i18n'; -import type { Doc, Store } from '@blocksuite/affine/store'; -import { BlockStdScope, type EditorHost } from '@blocksuite/std'; import { type Signal, signal } from '@preact/signals-core'; import { useFramework, useService } from '@toeverything/infra'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -52,7 +50,6 @@ export const Component = () => { const framework = useFramework(); const [isBodyProvided, setIsBodyProvided] = useState(false); const [isHeaderProvided, setIsHeaderProvided] = useState(false); - const [host, setHost] = useState(null); const [chatContent, setChatContent] = useState(null); const [chatTool, setChatTool] = useState(null); const [currentSession, setCurrentSession] = useState( @@ -132,28 +129,11 @@ export const Component = () => { [chatContent, chatTool, client, isOpeningSession, workspaceId] ); - // create a temp doc/host for ai-chat-content - useEffect(() => { - let tempDoc: Doc | null = null; - const collection = getCollection(); - const doc = collection.createDoc(); - tempDoc = doc; - doc.load(() => { - const host = new BlockStdScope({ - store: tempDoc?.getStore() as Store, - extensions: getCustomPageEditorBlockSpecs(), - }).render(); - setHost(host); - }); - - return () => { - tempDoc?.dispose(); - }; - }, []); + const confirmModal = useConfirmModal(); // init or update ai-chat-content useEffect(() => { - if (!isBodyProvided || !host) { + if (!isBodyProvided) { return; } @@ -164,7 +144,6 @@ export const Component = () => { } content.session = currentSession; - content.host = host; content.workspaceId = workspaceId; content.docDisplayConfig = docDisplayConfig; content.searchMenuConfig = searchMenuConfig; @@ -174,6 +153,11 @@ export const Component = () => { content.affineWorkspaceDialogService = framework.get( WorkspaceDialogService ); + content.affineThemeService = framework.get(AppThemeService); + content.notificationService = new NotificationServiceImpl( + confirmModal.closeConfirmModal, + confirmModal.openConfirmModal + ); if (!chatContent) { // initial values that won't change @@ -190,12 +174,12 @@ export const Component = () => { currentSession, docDisplayConfig, framework, - host, isBodyProvided, networkSearchConfig, reasoningConfig, searchMenuConfig, workspaceId, + confirmModal, ]); // init or update header ai-chat-toolbar @@ -213,6 +197,10 @@ export const Component = () => { tool.workspaceId = workspaceId; tool.docDisplayConfig = docDisplayConfig; tool.onOpenSession = onOpenSession; + tool.notificationService = new NotificationServiceImpl( + confirmModal.closeConfirmModal, + confirmModal.openConfirmModal + ); tool.onNewSession = () => { if (!currentSession) return; @@ -239,6 +227,7 @@ export const Component = () => { onOpenSession, togglePin, workspaceId, + confirmModal, ]); const onChatContainerRef = useCallback((node: HTMLDivElement) => { 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 d6def1768..f05b1582e 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,8 +1,11 @@ +import { useConfirmModal } from '@affine/component'; import { AIProvider, ChatPanel } from '@affine/core/blocksuite/ai'; 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'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { AppThemeService } from '@affine/core/modules/theme'; import { WorkbenchService } from '@affine/core/modules/workbench'; import { ViewExtensionManagerIdentifier } from '@blocksuite/affine/ext-loader'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; @@ -51,6 +54,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel( reasoningConfig, playgroundConfig, } = useAIChatConfig(); + const confirmModal = useConfirmModal(); useEffect(() => { if (!editor || !editor.host) return; @@ -87,6 +91,11 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel( ); chatPanelRef.current.affineWorkbenchService = framework.get(WorkbenchService); + chatPanelRef.current.affineThemeService = framework.get(AppThemeService); + chatPanelRef.current.notificationService = new NotificationServiceImpl( + confirmModal.closeConfirmModal, + confirmModal.openConfirmModal + ); containerRef.current?.append(chatPanelRef.current); } else { @@ -117,6 +126,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel( searchMenuConfig, reasoningConfig, playgroundConfig, + confirmModal, ]); const [autoResized, setAutoResized] = useState(false); diff --git a/packages/frontend/core/src/modules/theme/entities/theme.ts b/packages/frontend/core/src/modules/theme/entities/theme.ts index 0aa3205c4..b1fe01d47 100644 --- a/packages/frontend/core/src/modules/theme/entities/theme.ts +++ b/packages/frontend/core/src/modules/theme/entities/theme.ts @@ -1,5 +1,22 @@ +import { ColorScheme } from '@blocksuite/affine/model'; +import { createSignalFromObservable } from '@blocksuite/affine-shared/utils'; +import type { Signal } from '@preact/signals-core'; import { Entity, LiveData } from '@toeverything/infra'; export class AppTheme extends Entity { theme$ = new LiveData(undefined); + + themeSignal: Signal; + + constructor() { + super(); + const { signal, cleanup } = createSignalFromObservable( + this.theme$.map(theme => + theme === 'dark' ? ColorScheme.Dark : ColorScheme.Light + ), + ColorScheme.Light + ); + this.themeSignal = signal; + this.disposables.push(cleanup); + } } diff --git a/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.tsx b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.tsx index a17414145..bb05e7f19 100644 --- a/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.tsx +++ b/packages/frontend/core/src/modules/workbench/view/sidebar/sidebar-header.tsx @@ -26,7 +26,7 @@ function Container({ const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => { return ( - + ); diff --git a/tests/affine-cloud-copilot/e2e/basic/authority.spec.ts b/tests/affine-cloud-copilot/e2e/basic/authority.spec.ts index 68a51c106..de3b2c1f8 100644 --- a/tests/affine-cloud-copilot/e2e/basic/authority.spec.ts +++ b/tests/affine-cloud-copilot/e2e/basic/authority.spec.ts @@ -19,14 +19,14 @@ test.describe('AIBasic/Authority', () => { page, utils, }) => { - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await expect(page.getByTestId('ai-error')).toBeVisible(); await expect(page.getByTestId('ai-error-action-button')).toBeVisible(); }); test('should support login in error state', async ({ page, utils }) => { - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); const loginButton = page.getByTestId('ai-error-action-button'); await loginButton.click(); diff --git a/tests/affine-cloud-copilot/e2e/insertion/add-to-edgeless-as-note.spec.ts b/tests/affine-cloud-copilot/e2e/insertion/add-to-edgeless-as-note.spec.ts index 6419f177f..7db52b038 100644 --- a/tests/affine-cloud-copilot/e2e/insertion/add-to-edgeless-as-note.spec.ts +++ b/tests/affine-cloud-copilot/e2e/insertion/add-to-edgeless-as-note.spec.ts @@ -13,12 +13,12 @@ test.describe('AIInsertion/AddToEdgelessAsNote', () => { utils, }) => { await utils.editor.focusToEditor(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', @@ -47,12 +47,12 @@ test.describe('AIInsertion/AddToEdgelessAsNote', () => { await page.keyboard.press('Delete'); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', diff --git a/tests/affine-cloud-copilot/e2e/insertion/insert.spec.ts b/tests/affine-cloud-copilot/e2e/insertion/insert.spec.ts index cf1d110ed..bc1e16bd4 100644 --- a/tests/affine-cloud-copilot/e2e/insertion/insert.spec.ts +++ b/tests/affine-cloud-copilot/e2e/insertion/insert.spec.ts @@ -22,12 +22,12 @@ test.describe('AIInsertion/Insert', () => { await page.keyboard.insertText('World Block'); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', @@ -60,12 +60,12 @@ test.describe('AIInsertion/Insert', () => { await page.keyboard.insertText('World Block'); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', @@ -101,12 +101,12 @@ test.describe('AIInsertion/Insert', () => { await page.keyboard.insertText('World Block'); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', @@ -139,12 +139,12 @@ test.describe('AIInsertion/Insert', () => { await page.keyboard.insertText('World Block'); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', @@ -173,12 +173,12 @@ test.describe('AIInsertion/Insert', () => { await page.keyboard.press('Delete'); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', diff --git a/tests/affine-cloud-copilot/e2e/insertion/save-as-block.spec.ts b/tests/affine-cloud-copilot/e2e/insertion/save-as-block.spec.ts index 3852bbd94..89b726ad6 100644 --- a/tests/affine-cloud-copilot/e2e/insertion/save-as-block.spec.ts +++ b/tests/affine-cloud-copilot/e2e/insertion/save-as-block.spec.ts @@ -13,12 +13,12 @@ test.describe('AIInsertion/SaveAsBlock', () => { utils, }) => { await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', @@ -45,12 +45,12 @@ test.describe('AIInsertion/SaveAsBlock', () => { await utils.editor.switchToEdgelessMode(page); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', diff --git a/tests/affine-cloud-copilot/e2e/insertion/save-as-doc.spec.ts b/tests/affine-cloud-copilot/e2e/insertion/save-as-doc.spec.ts index d84eb4b79..afbc98554 100644 --- a/tests/affine-cloud-copilot/e2e/insertion/save-as-doc.spec.ts +++ b/tests/affine-cloud-copilot/e2e/insertion/save-as-doc.spec.ts @@ -13,12 +13,12 @@ test.describe('AIInsertion/SaveAsDoc', () => { utils, }) => { await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', @@ -45,12 +45,12 @@ test.describe('AIInsertion/SaveAsDoc', () => { await utils.editor.switchToEdgelessMode(page); await utils.chatPanel.openChatPanel(page); - await utils.chatPanel.makeChat(page, 'Hello'); + await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.'); await utils.chatPanel.waitForHistory(page, [ { role: 'user', - content: 'Hello', + content: 'Hello. Answer in 50 words.', }, { role: 'assistant', diff --git a/tests/affine-cloud-copilot/e2e/utils/chat-panel-utils.ts b/tests/affine-cloud-copilot/e2e/utils/chat-panel-utils.ts index 2397a9d60..0cb4eab45 100644 --- a/tests/affine-cloud-copilot/e2e/utils/chat-panel-utils.ts +++ b/tests/affine-cloud-copilot/e2e/utils/chat-panel-utils.ts @@ -37,7 +37,7 @@ export class ChatPanelUtils { } public static async closeChatPanel(page: Page) { - await page.getByTestId('right-sidebar-toggle').click({ + await page.getByTestId('right-sidebar-close').click({ delay: 200, }); await expect(page.getByTestId('sidebar-tab-content-chat')).toBeHidden();