feat(core): completely remove the dependence on EditorHost (#13110)

Close [AI-260](https://linear.app/affine-design/issue/AI-260)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added theme support to AI chat and message components, enabling
dynamic theming based on the current app theme.
* Introduced a reactive theme signal to the theme service for improved
theme handling.
* Integrated notification and theme services across various AI chat,
playground, and message components for consistent user experience.

* **Refactor**
* Simplified component APIs by removing dependencies on editor host and
related properties across AI chat, message, and tool components.
* Centralized and streamlined clipboard and markdown conversion
utilities, reducing external dependencies.
* Standardized the interface for context file addition and improved type
usage for better consistency.
* Reworked notification service to a class-based implementation for
improved encapsulation.
* Updated AI chat components to use injected notification and theme
services instead of host-based retrieval.

* **Bug Fixes**
* Improved reliability of copy and notification actions by decoupling
them from editor host dependencies.

* **Chores**
* Updated and cleaned up internal imports and removed unused properties
to enhance maintainability.
  * Added test IDs for sidebar close button to improve test reliability.
  * Updated test prompts in end-to-end tests for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-09 18:16:55 +08:00
committed by GitHub
parent dace1d1738
commit d10e5ee92f
48 changed files with 523 additions and 435 deletions

View File

@@ -40,7 +40,6 @@ export interface NotificationService {
}[]; }[];
onClose?: () => void; onClose?: () => void;
}): void; }): void;
/** /**
* Notify with undo action, it is a helper function to notify with undo action. * 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. * 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<NotificationService>(
); );
export function NotificationExtension( export function NotificationExtension(
notificationService: Omit<NotificationService, 'notifyWithUndoAction'> notificationService: NotificationService
): ExtensionType { ): ExtensionType {
return { return {
setup: di => { setup: di => {
di.addImpl(NotificationProvider, provider => { di.addImpl(NotificationProvider, provider => {
return { return {
...notificationService, notify: notificationService.notify,
toast: notificationService.toast,
confirm: notificationService.confirm,
prompt: notificationService.prompt,
notifyWithUndoAction: options => { notifyWithUndoAction: options => {
notifyWithUndoActionImpl( notifyWithUndoActionImpl(
provider, provider,

View File

@@ -31,7 +31,7 @@ export interface BlockStdOptions {
extensions: ExtensionType[]; extensions: ExtensionType[];
} }
const internalExtensions = [ export const internalExtensions = [
ServiceManager, ServiceManager,
CommandManager, CommandManager,
UIEventDispatcher, UIEventDispatcher,

View File

@@ -1,4 +1,5 @@
import type { import type {
AddContextFileInput,
ContextMatchedDocChunk, ContextMatchedDocChunk,
ContextMatchedFileChunk, ContextMatchedFileChunk,
ContextWorkspaceEmbeddingStatus, ContextWorkspaceEmbeddingStatus,
@@ -295,10 +296,7 @@ declare global {
}) => Promise<boolean>; }) => Promise<boolean>;
addContextFile: ( addContextFile: (
file: File, file: File,
options: { options: AddContextFileInput
contextId: string;
blobId: string;
}
) => Promise<CopilotContextFile>; ) => Promise<CopilotContextFile>;
removeContextFile: (options: { removeContextFile: (options: {
contextId: string; contextId: string;

View File

@@ -1,5 +1,9 @@
import type { TextRendererOptions } from '@affine/core/blocksuite/ai/components/text-renderer'; import type { TextRendererOptions } from '@affine/core/blocksuite/ai/components/text-renderer';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import {
NotificationProvider,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import { css, html, LitElement } from 'lit'; import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
@@ -65,6 +69,7 @@ export class AIChatBlockMessage extends LitElement {
} }
private renderStreamObjects(answer: StreamObject[]) { private renderStreamObjects(answer: StreamObject[]) {
const notificationService = this.host.std.get(NotificationProvider);
return html`<chat-content-stream-objects return html`<chat-content-stream-objects
.answer=${answer} .answer=${answer}
.host=${this.host} .host=${this.host}
@@ -72,17 +77,19 @@ export class AIChatBlockMessage extends LitElement {
.extensions=${this.textRendererOptions.extensions} .extensions=${this.textRendererOptions.extensions}
.affineFeatureFlagService=${this.textRendererOptions .affineFeatureFlagService=${this.textRendererOptions
.affineFeatureFlagService} .affineFeatureFlagService}
.notificationService=${notificationService}
.theme=${this.host.std.get(ThemeProvider).app$}
></chat-content-stream-objects>`; ></chat-content-stream-objects>`;
} }
private renderRichText(text: string) { private renderRichText(text: string) {
return html`<chat-content-rich-text return html`<chat-content-rich-text
.host=${this.host}
.text=${text} .text=${text}
.state=${this.state} .state=${this.state}
.extensions=${this.textRendererOptions.extensions} .extensions=${this.textRendererOptions.extensions}
.affineFeatureFlagService=${this.textRendererOptions .affineFeatureFlagService=${this.textRendererOptions
.affineFeatureFlagService} .affineFeatureFlagService}
.theme=${this.host.std.get(ThemeProvider).app$}
></chat-content-rich-text>`; ></chat-content-rich-text>`;
} }

View File

@@ -1,6 +1,7 @@
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { import {
ArrowDownBigIcon as ArrowDownIcon, ArrowDownBigIcon as ArrowDownIcon,
ArrowUpBigIcon as ArrowUpIcon, ArrowUpBigIcon as ArrowUpIcon,
@@ -163,16 +164,18 @@ export class ActionWrapper extends WithDisposable(LitElement) {
></chat-content-images>` ></chat-content-images>`
: nothing} : nothing}
${answer ${answer
? createTextRenderer(this.host, { ? createTextRenderer({
customHeading: true, customHeading: true,
testId: 'chat-message-action-answer', testId: 'chat-message-action-answer',
theme: this.host.std.get(ThemeProvider).app$,
})(answer) })(answer)
: nothing} : nothing}
${originalText ${originalText
? html`<div class="subtitle prompt">Prompt</div> ? html`<div class="subtitle prompt">Prompt</div>
${createTextRenderer(this.host, { ${createTextRenderer({
customHeading: true, customHeading: true,
testId: 'chat-message-action-prompt', testId: 'chat-message-action-prompt',
theme: this.host.std.get(ThemeProvider).app$,
})(item.messages[0].content + originalText)}` })(item.messages[0].content + originalText)}`
: nothing} : nothing}
</div> </div>

View File

@@ -3,6 +3,7 @@ import './action-wrapper';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVar } from '@blocksuite/affine/shared/theme';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { css, html, LitElement } from 'lit'; import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
@@ -57,8 +58,9 @@ export class ActionText extends WithDisposable(LitElement) {
class="original-text" class="original-text"
data-testid="original-text" data-testid="original-text"
> >
${createTextRenderer(this.host, { ${createTextRenderer({
customHeading: true, customHeading: true,
theme: this.host.std.get(ThemeProvider).app$,
})(originalText)} })(originalText)}
</div> </div>
</action-wrapper>`; </action-wrapper>`;

View File

@@ -1,5 +1,6 @@
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; 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 { WorkbenchService } from '@affine/core/modules/workbench';
import type { import type {
ContextEmbedStatus, ContextEmbedStatus,
@@ -7,7 +8,7 @@ import type {
UpdateChatSessionInput, UpdateChatSessionInput,
} from '@affine/graphql'; } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; 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 { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
@@ -125,6 +126,12 @@ export class ChatPanel extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor affineWorkbenchService!: WorkbenchService; accessor affineWorkbenchService!: WorkbenchService;
@property({ attribute: false })
accessor affineThemeService!: AppThemeService;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@state() @state()
accessor session: CopilotChatHistoryFragment | null | undefined; accessor session: CopilotChatHistoryFragment | null | undefined;
@@ -144,7 +151,6 @@ export class ChatPanel extends SignalWatcher(
private get chatTitle() { private get chatTitle() {
const [done, total] = this.embeddingProgress; const [done, total] = this.embeddingProgress;
const isEmbedding = total > 0 && done < total; const isEmbedding = total > 0 && done < total;
const notification = this.host.std.getOptional(NotificationProvider);
return html` return html`
<div class="chat-panel-title-text"> <div class="chat-panel-title-text">
@@ -170,7 +176,7 @@ export class ChatPanel extends SignalWatcher(
.onOpenSession=${this.openSession} .onOpenSession=${this.openSession}
.onOpenDoc=${this.openDoc} .onOpenDoc=${this.openDoc}
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.notification=${notification} .notificationService=${this.notificationService}
></ai-chat-toolbar> ></ai-chat-toolbar>
`; `;
} }
@@ -371,6 +377,8 @@ export class ChatPanel extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
></playground-content> ></playground-content>
`; `;
@@ -444,6 +452,8 @@ export class ChatPanel extends SignalWatcher(
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService} .affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange} .onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
.onContextChange=${this.onContextChange} .onContextChange=${this.onContextChange}
.width=${this.sidebarWidth} .width=${this.sidebarWidth}

View File

@@ -1,10 +1,12 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql'; import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils'; import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import type { Signal } from '@preact/signals-core'; import type { Signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit'; import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
@@ -35,9 +37,6 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor host: EditorHost | null | undefined; accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor docId: string | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor item!: ChatMessage; accessor item!: ChatMessage;
@@ -56,6 +55,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor affineThemeService!: AppThemeService;
@property({ attribute: false }) @property({ attribute: false })
accessor session!: CopilotChatHistoryFragment | null | undefined; accessor session!: CopilotChatHistoryFragment | null | undefined;
@@ -68,6 +70,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor width: Signal<number | undefined> | undefined; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor notificationService!: NotificationService;
get state() { get state() {
const { isLast, status } = this; const { isLast, status } = this;
return isLast return isLast
@@ -118,27 +123,29 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
private renderStreamObjects(answer: StreamObject[]) { private renderStreamObjects(answer: StreamObject[]) {
return html`<chat-content-stream-objects return html`<chat-content-stream-objects
.answer=${answer}
.host=${this.host} .host=${this.host}
.answer=${answer}
.state=${this.state} .state=${this.state}
.width=${this.width} .width=${this.width}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.notificationService=${this.notificationService}
.theme=${this.affineThemeService.appTheme.themeSignal}
></chat-content-stream-objects>`; ></chat-content-stream-objects>`;
} }
private renderRichText(text: string) { private renderRichText(text: string) {
return html`<chat-content-rich-text return html`<chat-content-rich-text
.host=${this.host}
.text=${text} .text=${text}
.state=${this.state} .state=${this.state}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.theme=${this.affineThemeService.appTheme.themeSignal}
></chat-content-rich-text>`; ></chat-content-rich-text>`;
} }
private renderEditorActions() { 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; if (!isChatMessage(item) || item.role !== 'assistant') return nothing;
@@ -161,7 +168,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
: EdgelessEditorActions : EdgelessEditorActions
: null; : null;
const showActions = host && docId && !!markdown; const showActions = host && !!markdown;
return html` return html`
<chat-copy-more <chat-copy-more
@@ -173,6 +180,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.messageId=${messageId} .messageId=${messageId}
.withMargin=${true} .withMargin=${true}
.retry=${() => this.retry()} .retry=${() => this.retry()}
.notificationService=${this.notificationService}
></chat-copy-more> ></chat-copy-more>
${isLast && showActions ${isLast && showActions
? html`<chat-action-list ? html`<chat-action-list
@@ -182,6 +190,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.content=${markdown} .content=${markdown}
.messageId=${messageId ?? undefined} .messageId=${messageId ?? undefined}
.withMargin=${true} .withMargin=${true}
.notificationService=${this.notificationService}
></chat-action-list>` ></chat-action-list>`
: nothing} : nothing}
`; `;

View File

@@ -2,7 +2,7 @@ import type { TagMeta } from '@affine/core/components/page-list';
import { createLitPortal } from '@blocksuite/affine/components/portal'; import { createLitPortal } from '@blocksuite/affine/components/portal';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; 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 { MoreVerticalIcon, PlusIcon } from '@blocksuite/icons/lit';
import { flip, offset } from '@floating-ui/dom'; import { flip, offset } from '@floating-ui/dom';
import { computed, type Signal, signal } from '@preact/signals-core'; import { computed, type Signal, signal } from '@preact/signals-core';
@@ -82,9 +82,6 @@ export class ChatPanelChips extends SignalWatcher(
private _abortController: AbortController | null = null; private _abortController: AbortController | null = null;
@property({ attribute: false })
accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor chips!: ChatChip[]; accessor chips!: ChatChip[];
@@ -167,7 +164,6 @@ export class ChatPanelChips extends SignalWatcher(
.removeChip=${this._removeChip} .removeChip=${this._removeChip}
.checkTokenLimit=${this._checkTokenLimit} .checkTokenLimit=${this._checkTokenLimit}
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.host=${this.host}
></chat-panel-doc-chip>`; ></chat-panel-doc-chip>`;
} }
if (isFileChip(chip)) { if (isFileChip(chip)) {
@@ -407,13 +403,8 @@ export class ChatPanelChips extends SignalWatcher(
if (!contextId || !AIProvider.context) { if (!contextId || !AIProvider.context) {
throw new Error('Context not found'); 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, { const contextFile = await AIProvider.context.addContextFile(chip.file, {
contextId, contextId,
blobId,
}); });
this._updateChip(chip, { this._updateChip(chip, {
state: contextFile.status, state: contextFile.status,

View File

@@ -1,6 +1,6 @@
import track from '@affine/track'; import track from '@affine/track';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; 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 { Signal } from '@preact/signals-core';
import { html, type PropertyValues } from 'lit'; import { html, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
@@ -36,9 +36,6 @@ export class ChatPanelDocChip extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig; accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor host: EditorHost | null | undefined;
private chipName = new Signal<string>(''); private chipName = new Signal<string>('');
override connectedCallback() { override connectedCallback() {
@@ -103,9 +100,6 @@ export class ChatPanelDocChip extends SignalWatcher(
}; };
private readonly processDocChip = async () => { private readonly processDocChip = async () => {
if (!this.host) {
return;
}
try { try {
const doc = this.docDisplayConfig.getDoc(this.chip.docId); const doc = this.docDisplayConfig.getDoc(this.chip.docId);
if (!doc) { if (!doc) {
@@ -114,10 +108,7 @@ export class ChatPanelDocChip extends SignalWatcher(
if (!doc.ready) { if (!doc.ready) {
doc.load(); doc.load();
} }
const value = await extractMarkdownFromDoc( const value = await extractMarkdownFromDoc(doc);
doc,
this.host.std.store.provider
);
const tokenCount = estimateTokenCount(value); const tokenCount = estimateTokenCount(value);
if (this.checkTokenLimit(this.chip, tokenCount)) { if (this.checkTokenLimit(this.chip, tokenCount)) {
const markdown = this.chip.markdown ?? new Signal<string>(''); const markdown = this.chip.markdown ?? new Signal<string>('');

View File

@@ -120,7 +120,6 @@ export class AIChatComposer extends SignalWatcher(
override render() { override render() {
return html` return html`
<chat-panel-chips <chat-panel-chips
.host=${this.host}
.chips=${this.chips} .chips=${this.chips}
.createContextId=${this._createContextId} .createContextId=${this._createContextId}
.updateChips=${this.updateChips} .updateChips=${this.updateChips}

View File

@@ -1,5 +1,6 @@
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { import type {
ContextEmbedStatus, ContextEmbedStatus,
CopilotChatHistoryFragment, CopilotChatHistoryFragment,
@@ -8,6 +9,7 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import { type Signal } from '@preact/signals-core'; import { type Signal } from '@preact/signals-core';
import { import {
css, css,
@@ -160,6 +162,12 @@ export class AIChatContent extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService; accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor affineThemeService!: AppThemeService;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false }) @property({ attribute: false })
accessor onEmbeddingProgressChange!: ( accessor onEmbeddingProgressChange!: (
count: Record<ContextEmbedStatus, number> count: Record<ContextEmbedStatus, number>
@@ -401,6 +409,8 @@ export class AIChatContent extends SignalWatcher(
.isHistoryLoading=${this.isHistoryLoading} .isHistoryLoading=${this.isHistoryLoading}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.networkSearchConfig=${this.networkSearchConfig} .networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig} .reasoningConfig=${this.reasoningConfig}
.width=${this.width} .width=${this.width}

View File

@@ -1,8 +1,10 @@
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql'; import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { import {
DocModeProvider, DocModeProvider,
FeatureFlagService, type FeatureFlagService,
type NotificationService,
} from '@blocksuite/affine/shared/services'; } from '@blocksuite/affine/shared/services';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
@@ -187,6 +189,12 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor affineThemeService!: AppThemeService;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false }) @property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig; accessor networkSearchConfig!: AINetworkSearchConfig;
@@ -222,8 +230,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
} }
private _renderAIOnboarding() { private _renderAIOnboarding() {
return this.isHistoryLoading || return this.isHistoryLoading
!this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding')
? nothing ? nothing
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding"> : html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
${repeat( ${repeat(
@@ -311,7 +318,6 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
} else if (isChatMessage(item) && item.role === 'assistant') { } else if (isChatMessage(item) && item.role === 'assistant') {
return html`<chat-message-assistant return html`<chat-message-assistant
.host=${this.host} .host=${this.host}
.docId=${this.docId}
.session=${this.session} .session=${this.session}
.item=${item} .item=${item}
.isLast=${isLast} .isLast=${isLast}
@@ -319,6 +325,8 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
.error=${isLast ? error : null} .error=${isLast ? error : null}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.retry=${() => this.retry()} .retry=${() => this.retry()}
.width=${this.width} .width=${this.width}
></chat-message-assistant>`; ></chat-message-assistant>`;

View File

@@ -42,7 +42,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
accessor docDisplayConfig!: DocDisplayConfig; accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor notification: NotificationService | null | undefined; accessor notificationService!: NotificationService;
@query('.history-button') @query('.history-button')
accessor historyButton!: HTMLDivElement; accessor historyButton!: HTMLDivElement;
@@ -104,21 +104,19 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
private readonly unpinConfirm = async () => { private readonly unpinConfirm = async () => {
if (this.session && this.session.pinned) { if (this.session && this.session.pinned) {
try { try {
const confirm = this.notification const confirm = await this.notificationService.confirm({
? await this.notification.confirm({ title: 'Switch Chat? Current chat is pinned',
title: 'Switch Chat? Current chat is pinned', message:
message: 'Switching will unpinned the current chat. This will change the active chat panel, allowing you to navigate between different conversation histories.',
'Switching will unpinned the current chat. This will change the active chat panel, allowing you to navigate between different conversation histories.', confirmText: 'Switch Chat',
confirmText: 'Switch Chat', cancelText: 'Cancel',
cancelText: 'Cancel', });
})
: true;
if (!confirm) { if (!confirm) {
return false; return false;
} }
await this.onTogglePin(); await this.onTogglePin();
} catch { } catch {
this.notification?.toast('Failed to unpin the chat'); this.notificationService.toast('Failed to unpin the chat');
} }
} }
return true; return true;
@@ -133,7 +131,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
private readonly onSessionClick = async (sessionId: string) => { private readonly onSessionClick = async (sessionId: string) => {
if (this.session?.sessionId === sessionId) { if (this.session?.sessionId === sessionId) {
this.notification?.toast('You are already in this chat'); this.notificationService.toast('You are already in this chat');
return; return;
} }
const confirm = await this.unpinConfirm(); const confirm = await this.unpinConfirm();
@@ -144,7 +142,7 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
private readonly onDocClick = async (docId: string, sessionId: string) => { private readonly onDocClick = async (docId: string, sessionId: string) => {
if (this.docId === docId && this.session?.sessionId === sessionId) { 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; return;
} }
this.onOpenDoc(docId, sessionId); this.onOpenDoc(docId, sessionId);
@@ -169,7 +167,6 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.onSessionClick=${this.onSessionClick} .onSessionClick=${this.onSessionClick}
.onDocClick=${this.onDocClick} .onDocClick=${this.onDocClick}
.notification=${this.notification}
></ai-session-history> ></ai-session-history>
`, `,
portalStyles: { portalStyles: {

View File

@@ -1,6 +1,5 @@
import type { CopilotSessionType } from '@affine/graphql'; import type { CopilotSessionType } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { NotificationService } from '@blocksuite/affine/shared/services';
import { scrollbarStyle } from '@blocksuite/affine/shared/styles'; import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
@@ -134,9 +133,6 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor onDocClick!: (docId: string, sessionId: string) => void; accessor onDocClick!: (docId: string, sessionId: string) => void;
@property({ attribute: false })
accessor notification: NotificationService | null | undefined;
@state() @state()
private accessor sessions: BlockSuitePresets.AIRecentSession[] = []; private accessor sessions: BlockSuitePresets.AIRecentSession[] = [];

View File

@@ -18,7 +18,7 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
accessor session!: CopilotChatHistoryFragment | null | undefined; accessor session!: CopilotChatHistoryFragment | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor notification: NotificationService | null | undefined; accessor notificationService!: NotificationService;
@property({ attribute: false }) @property({ attribute: false })
accessor doc!: Store; accessor doc!: Store;
@@ -52,15 +52,13 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
} }
const sessionId = this.session.sessionId; const sessionId = this.session.sessionId;
try { try {
const confirm = this.notification const confirm = await this.notificationService.confirm({
? await this.notification.confirm({ title: 'Clear History',
title: 'Clear History', message:
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.',
'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',
confirmText: 'Confirm', cancelText: 'Cancel',
cancelText: 'Cancel', });
})
: true;
if (confirm) { if (confirm) {
const actionIds = this.chatContextValue.messages const actionIds = this.chatContextValue.messages
@@ -71,11 +69,11 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
this.doc.id, this.doc.id,
[...(sessionId ? [sessionId] : []), ...(actionIds || [])] [...(sessionId ? [sessionId] : []), ...(actionIds || [])]
); );
this.notification?.toast('History cleared'); this.notificationService.toast('History cleared');
this.onHistoryCleared?.(); this.onHistoryCleared?.();
} }
} catch { } catch {
this.notification?.toast('Failed to clear history'); this.notificationService.toast('Failed to clear history');
} }
}; };

View File

@@ -1,17 +1,15 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WithDisposable } from '@blocksuite/affine/global/lit'; 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 { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import type { Signal } from '@preact/signals-core';
import { html } from 'lit'; import { html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { createTextRenderer } from '../../components/text-renderer'; import { createTextRenderer } from '../../components/text-renderer';
export class ChatContentRichText extends WithDisposable(ShadowlessElement) { export class ChatContentRichText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor text!: string; accessor text!: string;
@@ -24,12 +22,16 @@ export class ChatContentRichText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor theme!: Signal<ColorScheme>;
protected override render() { protected override render() {
const { text, host } = this; const { text } = this;
return html`${createTextRenderer(host, { return html`${createTextRenderer({
customHeading: true, customHeading: true,
extensions: this.extensions, extensions: this.extensions,
affineFeatureFlagService: this.affineFeatureFlagService, affineFeatureFlagService: this.affineFeatureFlagService,
theme: this.theme,
})(text, this.state)}`; })(text, this.state)}`;
} }
} }

View File

@@ -1,14 +1,13 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { ImageProxyService } from '@blocksuite/affine/shared/adapters'; import type { ColorScheme } from '@blocksuite/affine/model';
import type { EditorHost } from '@blocksuite/affine/std'; import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import type { Signal } from '@preact/signals-core'; import type { Signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit'; import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { BlockDiffProvider } from '../../services/block-diff';
import type { AffineAIPanelState } from '../../widgets/ai-panel/type'; import type { AffineAIPanelState } from '../../widgets/ai-panel/type';
import type { StreamObject } from '../ai-chat-messages'; import type { StreamObject } from '../ai-chat-messages';
@@ -42,12 +41,16 @@ export class ChatContentStreamObjects extends WithDisposable(
@property({ attribute: false }) @property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor theme!: Signal<ColorScheme>;
@property({ attribute: false })
accessor notificationService!: NotificationService;
private renderToolCall(streamObject: StreamObject) { private renderToolCall(streamObject: StreamObject) {
if (streamObject.type !== 'tool-call') { if (streamObject.type !== 'tool-call') {
return nothing; return nothing;
} }
const imageProxyService = this.host?.store.get(ImageProxyService);
const blockDiffService = this.host?.view.std.getOptional(BlockDiffProvider);
switch (streamObject.toolName) { switch (streamObject.toolName) {
case 'web_crawl_exa': case 'web_crawl_exa':
@@ -55,7 +58,6 @@ export class ChatContentStreamObjects extends WithDisposable(
<web-crawl-tool <web-crawl-tool
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-crawl-tool> ></web-crawl-tool>
`; `;
case 'web_search_exa': case 'web_search_exa':
@@ -63,7 +65,6 @@ export class ChatContentStreamObjects extends WithDisposable(
<web-search-tool <web-search-tool
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-search-tool> ></web-search-tool>
`; `;
case 'doc_compose': case 'doc_compose':
@@ -72,7 +73,8 @@ export class ChatContentStreamObjects extends WithDisposable(
.std=${this.host?.std} .std=${this.host?.std}
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService} .theme=${this.theme}
.notificationService=${this.notificationService}
></doc-compose-tool> ></doc-compose-tool>
`; `;
case 'code_artifact': case 'code_artifact':
@@ -81,7 +83,6 @@ export class ChatContentStreamObjects extends WithDisposable(
.std=${this.host?.std} .std=${this.host?.std}
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></code-artifact-tool> ></code-artifact-tool>
`; `;
case 'doc_edit': case 'doc_edit':
@@ -89,7 +90,7 @@ export class ChatContentStreamObjects extends WithDisposable(
<doc-edit-tool <doc-edit-tool
.data=${streamObject} .data=${streamObject}
.doc=${this.host?.store} .doc=${this.host?.store}
.blockDiffService=${blockDiffService} .notificationService=${this.notificationService}
></doc-edit-tool> ></doc-edit-tool>
`; `;
default: { default: {
@@ -105,8 +106,6 @@ export class ChatContentStreamObjects extends WithDisposable(
if (streamObject.type !== 'tool-result') { if (streamObject.type !== 'tool-result') {
return nothing; return nothing;
} }
const imageProxyService = this.host?.store.get(ImageProxyService);
const blockDiffService = this.host?.view.std.getOptional(BlockDiffProvider);
switch (streamObject.toolName) { switch (streamObject.toolName) {
case 'web_crawl_exa': case 'web_crawl_exa':
@@ -114,7 +113,6 @@ export class ChatContentStreamObjects extends WithDisposable(
<web-crawl-tool <web-crawl-tool
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-crawl-tool> ></web-crawl-tool>
`; `;
case 'web_search_exa': case 'web_search_exa':
@@ -122,7 +120,6 @@ export class ChatContentStreamObjects extends WithDisposable(
<web-search-tool <web-search-tool
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-search-tool> ></web-search-tool>
`; `;
case 'doc_compose': case 'doc_compose':
@@ -131,7 +128,8 @@ export class ChatContentStreamObjects extends WithDisposable(
.std=${this.host?.std} .std=${this.host?.std}
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService} .theme=${this.theme}
.notificationService=${this.notificationService}
></doc-compose-tool> ></doc-compose-tool>
`; `;
case 'code_artifact': case 'code_artifact':
@@ -140,7 +138,6 @@ export class ChatContentStreamObjects extends WithDisposable(
.std=${this.host?.std} .std=${this.host?.std}
.data=${streamObject} .data=${streamObject}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></code-artifact-tool> ></code-artifact-tool>
`; `;
case 'doc_edit': case 'doc_edit':
@@ -148,8 +145,8 @@ export class ChatContentStreamObjects extends WithDisposable(
<doc-edit-tool <doc-edit-tool
.data=${streamObject} .data=${streamObject}
.host=${this.host} .host=${this.host}
.blockDiffService=${blockDiffService}
.renderRichText=${this.renderRichText.bind(this)} .renderRichText=${this.renderRichText.bind(this)}
.notificationService=${this.notificationService}
></doc-edit-tool> ></doc-edit-tool>
`; `;
default: { default: {
@@ -158,7 +155,6 @@ export class ChatContentStreamObjects extends WithDisposable(
<tool-result-card <tool-result-card
.name=${name} .name=${name}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></tool-result-card> ></tool-result-card>
`; `;
} }
@@ -167,11 +163,11 @@ export class ChatContentStreamObjects extends WithDisposable(
private renderRichText(text: string) { private renderRichText(text: string) {
return html`<chat-content-rich-text return html`<chat-content-rich-text
.host=${this.host}
.text=${text} .text=${text}
.state=${this.state} .state=${this.state}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.theme=${this.theme}
></chat-content-rich-text>`; ></chat-content-rich-text>`;
} }

View File

@@ -62,7 +62,7 @@ export class AIScrollableTextRenderer extends WithDisposable(
} }
override render() { override render() {
const { host, answer, state, textRendererOptions } = this; const { answer, state, textRendererOptions } = this;
return html` <style> return html` <style>
.ai-scrollable-text-renderer { .ai-scrollable-text-renderer {
@@ -71,7 +71,6 @@ export class AIScrollableTextRenderer extends WithDisposable(
</style> </style>
<div class="ai-scrollable-text-renderer" @wheel=${this._onWheel}> <div class="ai-scrollable-text-renderer" @wheel=${this._onWheel}>
<text-renderer <text-renderer
.host=${host}
.answer=${answer} .answer=${answer}
.state=${state} .state=${state}
.options=${textRendererOptions} .options=${textRendererOptions}
@@ -83,7 +82,7 @@ export class AIScrollableTextRenderer extends WithDisposable(
accessor answer!: string; accessor answer!: string;
@property({ attribute: false }) @property({ attribute: false })
accessor host: EditorHost | null | undefined; accessor host!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor state: AffineAIPanelState | undefined; accessor state: AffineAIPanelState | undefined;

View File

@@ -1,7 +1,6 @@
import { CodeBlockHighlighter } from '@blocksuite/affine/blocks/code'; import { CodeBlockHighlighter } from '@blocksuite/affine/blocks/code';
import { toast } from '@blocksuite/affine/components/toast'; import { toast } from '@blocksuite/affine/components/toast';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { ImageProxyService } from '@blocksuite/affine/shared/adapters';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { type BlockStdScope, ShadowlessElement } from '@blocksuite/affine/std'; import { type BlockStdScope, ShadowlessElement } from '@blocksuite/affine/std';
import { CopyIcon, PageIcon, ToolIcon } from '@blocksuite/icons/lit'; import { CopyIcon, PageIcon, ToolIcon } from '@blocksuite/icons/lit';
@@ -296,9 +295,6 @@ export class CodeArtifactTool extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor width: Signal<number | undefined> | undefined; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor imageProxyService: ImageProxyService | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor std: BlockStdScope | undefined; accessor std: BlockStdScope | undefined;

View File

@@ -5,14 +5,11 @@ import { LoadingIcon } from '@blocksuite/affine/components/icons';
import { toast } from '@blocksuite/affine/components/toast'; import { toast } from '@blocksuite/affine/components/toast';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
import type { ImageProxyService } from '@blocksuite/affine/shared/adapters'; import type { ColorScheme } from '@blocksuite/affine/model';
import {
NotificationProvider,
ThemeProvider,
} from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { type BlockStdScope, ShadowlessElement } from '@blocksuite/affine/std'; import { type BlockStdScope, ShadowlessElement } from '@blocksuite/affine/std';
import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc'; 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 { CopyIcon, PageIcon, ToolIcon } from '@blocksuite/icons/lit';
import { type Signal } from '@preact/signals-core'; import { type Signal } from '@preact/signals-core';
import { css, html, nothing, type PropertyValues } from 'lit'; import { css, html, nothing, type PropertyValues } from 'lit';
@@ -110,10 +107,13 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) {
accessor width: Signal<number | undefined> | undefined; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor imageProxyService: ImageProxyService | null | undefined; accessor std: BlockStdScope | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor std: BlockStdScope | undefined; accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor theme!: Signal<ColorScheme>;
override updated(changedProperties: PropertyValues) { override updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
@@ -147,7 +147,6 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) {
return; return;
} }
const workspace = std.store.workspace; const workspace = std.store.workspace;
const notificationService = std.get(NotificationProvider);
const refNodeSlots = std.getOptional(RefNodeSlotsProvider); const refNodeSlots = std.getOptional(RefNodeSlotsProvider);
const docId = await MarkdownTransformer.importMarkdownToDoc({ const docId = await MarkdownTransformer.importMarkdownToDoc({
collection: workspace, collection: workspace,
@@ -157,7 +156,7 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) {
extensions: getStoreManager().config.init().value.get('store'), extensions: getStoreManager().config.init().value.get('store'),
}); });
if (docId) { if (docId) {
const open = await notificationService.confirm({ const open = await this.notificationService.confirm({
title: 'Open the doc you just created', title: 'Open the doc you just created',
message: 'Doc saved successfully! Would you like to open it now?', message: 'Doc saved successfully! Would you like to open it now?',
cancelText: 'Cancel', cancelText: 'Cancel',
@@ -200,8 +199,6 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) {
${successResult ${successResult
? html`<text-renderer ? html`<text-renderer
.answer=${successResult.markdown} .answer=${successResult.markdown}
.host=${std.host}
.schema=${std.store.schema}
.options=${{ .options=${{
customHeading: true, customHeading: true,
extensions: getCustomPageEditorBlockSpecs(), extensions: getCustomPageEditorBlockSpecs(),
@@ -237,10 +234,8 @@ export class DocComposeTool extends WithDisposable(ShadowlessElement) {
></tool-call-failed>`; ></tool-call-failed>`;
} }
const theme = this.std.get(ThemeProvider).theme;
const { LinkedDocEmptyBanner } = getEmbedLinkedDocIcons( const { LinkedDocEmptyBanner } = getEmbedLinkedDocIcons(
theme, this.theme.value,
'page', 'page',
'horizontal' 'horizontal'
); );

View File

@@ -1,7 +1,7 @@
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { NotificationProvider } from '@blocksuite/affine/shared/services';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import { import {
CloseIcon, CloseIcon,
CopyIcon, CopyIcon,
@@ -14,7 +14,7 @@ import {
import { css, html, nothing } from 'lit'; import { css, html, nothing } from 'lit';
import { property, state } from 'lit/decorators.js'; 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 { diffMarkdown } from '../../utils/apply-model/markdown-diff';
import { copyText } from '../../utils/editor-actions'; import { copyText } from '../../utils/editor-actions';
import type { ToolError } from './type'; import type { ToolError } from './type';
@@ -190,14 +190,18 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
accessor data!: DocEditToolCall | DocEditToolResult; accessor data!: DocEditToolCall | DocEditToolResult;
@property({ attribute: false }) @property({ attribute: false })
accessor blockDiffService: BlockDiffService | undefined; accessor renderRichText!: (text: string) => string;
@property({ attribute: false }) @property({ attribute: false })
accessor renderRichText!: (text: string) => string; accessor notificationService!: NotificationService;
@state() @state()
accessor isCollapsed = false; accessor isCollapsed = false;
get blockDiffService() {
return this.host?.std.getOptional(BlockDiffProvider);
}
private async _handleApply(markdown: string) { private async _handleApply(markdown: string) {
if (!this.host) { if (!this.host) {
return; return;
@@ -229,14 +233,9 @@ export class DocEditTool extends WithDisposable(ShadowlessElement) {
if (!this.host) { if (!this.host) {
return; return;
} }
const success = await copyText( const success = await copyText(removeMarkdownComments(changedMarkdown));
this.host,
removeMarkdownComments(changedMarkdown)
);
if (success) { if (success) {
const notificationService = this.notificationService.notify({
this.host?.std.getOptional(NotificationProvider);
notificationService?.notify({
title: 'Copied to clipboard', title: 'Copied to clipboard',
accent: 'success', accent: 'success',
onClose: function (): void {}, onClose: function (): void {},

View File

@@ -1,7 +1,7 @@
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { type ImageProxyService } from '@blocksuite/affine/shared/adapters';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts';
import { ToggleDownIcon, ToolIcon } from '@blocksuite/icons/lit'; import { ToggleDownIcon, ToolIcon } from '@blocksuite/icons/lit';
import { type Signal } from '@preact/signals-core'; import { type Signal } from '@preact/signals-core';
import { css, html, nothing, type TemplateResult } from 'lit'; import { css, html, nothing, type TemplateResult } from 'lit';
@@ -205,12 +205,11 @@ export class ToolResultCard extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor width: Signal<number | undefined> | undefined; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor imageProxyService: ImageProxyService | null | undefined;
@state() @state()
private accessor isCollapsed = true; private accessor isCollapsed = true;
private readonly imageProxyURL = DEFAULT_IMAGE_PROXY_ENDPOINT;
protected override render() { protected override render() {
return html` return html`
<div class="ai-tool-result-wrapper"> <div class="ai-tool-result-wrapper">
@@ -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) { private renderIcon(icon: string | TemplateResult<1> | undefined) {
if (!icon) { if (!icon) {
return nothing; return nothing;
} }
if (typeof icon === 'string') { if (typeof icon === 'string') {
if (this.imageProxyService) { return html`<img src=${this.buildUrl(icon)} />`;
return html`<img src=${this.imageProxyService.buildUrl(icon)} />`;
}
return html`<img src=${icon} />`;
} }
return html`${icon}`; return html`${icon}`;
} }

View File

@@ -1,5 +1,4 @@
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { ImageProxyService } from '@blocksuite/affine/shared/adapters';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
import { WebIcon } from '@blocksuite/icons/lit'; import { WebIcon } from '@blocksuite/icons/lit';
import type { Signal } from '@preact/signals-core'; import type { Signal } from '@preact/signals-core';
@@ -40,9 +39,6 @@ export class WebCrawlTool extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor width: Signal<number | undefined> | undefined; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor imageProxyService: ImageProxyService | null | undefined;
renderToolCall() { renderToolCall() {
return html` return html`
<tool-call-card <tool-call-card
@@ -73,7 +69,6 @@ export class WebCrawlTool extends WithDisposable(ShadowlessElement) {
}, },
]} ]}
.width=${this.width} .width=${this.width}
.imageProxyService=${this.imageProxyService}
></tool-result-card> ></tool-result-card>
`; `;
} }

View File

@@ -1,5 +1,4 @@
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { ImageProxyService } from '@blocksuite/affine/shared/adapters';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
import { WebIcon } from '@blocksuite/icons/lit'; import { WebIcon } from '@blocksuite/icons/lit';
import type { Signal } from '@preact/signals-core'; import type { Signal } from '@preact/signals-core';
@@ -40,9 +39,6 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor width: Signal<number | undefined> | undefined; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor imageProxyService: ImageProxyService | null | undefined;
renderToolCall() { renderToolCall() {
return html` return html`
<tool-call-card <tool-call-card
@@ -75,7 +71,6 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
.footerIcons=${footerIcons} .footerIcons=${footerIcons}
.results=${results} .results=${results}
.width=${this.width} .width=${this.width}
.imageProxyService=${this.imageProxyService}
></tool-result-card> ></tool-result-card>
`; `;
} }

View File

@@ -1,11 +1,11 @@
import type { CopilotChatHistoryFragment } from '@affine/graphql'; import type { CopilotChatHistoryFragment } from '@affine/graphql';
import type { ImageSelection } from '@blocksuite/affine/shared/selection'; import type { ImageSelection } from '@blocksuite/affine/shared/selection';
import { NotificationProvider } from '@blocksuite/affine/shared/services';
import type { import type {
BlockSelection, BlockSelection,
EditorHost, EditorHost,
TextSelection, TextSelection,
} from '@blocksuite/affine/std'; } from '@blocksuite/affine/std';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import { css, html, LitElement, nothing } from 'lit'; import { css, html, LitElement, nothing } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
@@ -96,6 +96,9 @@ export class ChatActionList extends LitElement {
@property({ attribute: 'data-testid', reflect: true }) @property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-action-list'; accessor testId = 'chat-action-list';
@property({ attribute: false })
accessor notificationService!: NotificationService;
override render() { override render() {
const { actions } = this; const { actions } = this;
if (!actions.length) { if (!actions.length) {
@@ -148,7 +151,7 @@ export class ChatActionList extends LitElement {
messageId messageId
); );
if (success) { if (success) {
this.host.std.getOptional(NotificationProvider)?.notify({ this.notificationService.notify({
title: action.toast, title: action.toast,
accent: 'success', accent: 'success',
onClose: function (): void {}, onClose: function (): void {},

View File

@@ -2,7 +2,6 @@ import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { Tooltip } from '@blocksuite/affine/components/toolbar'; import { Tooltip } from '@blocksuite/affine/components/toolbar';
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { noop } from '@blocksuite/affine/global/utils'; import { noop } from '@blocksuite/affine/global/utils';
import { NotificationProvider } from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { createButtonPopper } from '@blocksuite/affine/shared/utils'; import { createButtonPopper } from '@blocksuite/affine/shared/utils';
import type { import type {
@@ -10,6 +9,7 @@ import type {
EditorHost, EditorHost,
TextSelection, TextSelection,
} from '@blocksuite/affine/std'; } from '@blocksuite/affine/std';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import { CopyIcon, MoreHorizontalIcon, ResetIcon } from '@blocksuite/icons/lit'; import { CopyIcon, MoreHorizontalIcon, ResetIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement, nothing, type PropertyValues } from 'lit'; import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
@@ -131,14 +131,15 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
@property({ attribute: 'data-testid', reflect: true }) @property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-actions'; accessor testId = 'chat-actions';
@property({ attribute: false })
accessor notificationService!: NotificationService;
private _toggle() { private _toggle() {
this._morePopper?.toggle(); this._morePopper?.toggle();
} }
private readonly _notifySuccess = (title: string) => { private readonly _notifySuccess = (title: string) => {
const notificationService = this.notificationService.notify({
this.host?.std.getOptional(NotificationProvider);
notificationService?.notify({
title: title, title: title,
accent: 'success', accent: 'success',
onClose: function (): void {}, onClose: function (): void {},
@@ -165,7 +166,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
override render() { override render() {
const { host, content, isLast, messageId, actions } = this; const { host, content, isLast, messageId, actions } = this;
const showMoreIcon = !isLast && host && actions.length > 0; const showMoreIcon = !isLast && actions.length > 0;
return html`<style> return html`<style>
.copy-more { .copy-more {
margin-top: ${this.withMargin ? '8px' : '0px'}; margin-top: ${this.withMargin ? '8px' : '0px'};
@@ -176,11 +177,11 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
} }
</style> </style>
<div class="copy-more"> <div class="copy-more">
${content && host ${content
? html`<div ? html`<div
class="button copy" class="button copy"
@click=${async () => { @click=${async () => {
const success = await copyText(host, content); const success = await copyText(content);
if (success) { if (success) {
this._notifySuccess('Copied to clipboard'); this._notifySuccess('Copied to clipboard');
} }
@@ -201,7 +202,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
<affine-tooltip .autoShift=${true}>Retry</affine-tooltip> <affine-tooltip .autoShift=${true}>Retry</affine-tooltip>
</div>` </div>`
: nothing} : nothing}
${showMoreIcon ${showMoreIcon && host
? html`<div ? html`<div
class="button more" class="button more"
data-testid="action-more-button" data-testid="action-more-button"

View File

@@ -1,10 +1,11 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { import type {
ContextEmbedStatus, ContextEmbedStatus,
CopilotChatHistoryFragment, CopilotChatHistoryFragment,
} from '@affine/graphql'; } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; 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 { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
@@ -16,6 +17,7 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
import type { AppSidebarConfig } from '../../chat-panel/chat-config'; import type { AppSidebarConfig } from '../../chat-panel/chat-config';
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
import { AIProvider } from '../../provider'; import { AIProvider } from '../../provider';
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips'; import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
import type { ChatContextValue } from '../ai-chat-content'; import type { ChatContextValue } from '../ai-chat-content';
@@ -29,6 +31,7 @@ import {
type ChatAction, type ChatAction,
type ChatMessage, type ChatMessage,
type HistoryMessage, type HistoryMessage,
isChatMessage,
} from '../ai-chat-messages'; } from '../ai-chat-messages';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = { const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
@@ -159,6 +162,12 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor affineThemeService!: AppThemeService;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false }) @property({ attribute: false })
accessor addChat!: () => Promise<void>; accessor addChat!: () => Promise<void>;
@@ -177,6 +186,17 @@ export class PlaygroundChat extends SignalWatcher(
// request counter to track the latest request // request counter to track the latest request
private _updateHistoryCounter = 0; 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 () => { private readonly _initPanel = async () => {
const userId = (await AIProvider.userInfo)?.id; const userId = (await AIProvider.userInfo)?.id;
if (!userId) return; if (!userId) return;
@@ -276,7 +296,6 @@ export class PlaygroundChat extends SignalWatcher(
override render() { override render() {
const [done, total] = this.embeddingProgress; const [done, total] = this.embeddingProgress;
const isEmbedding = total > 0 && done < total; const isEmbedding = total > 0 && done < total;
const notification = this.host.std.getOptional(NotificationProvider);
return html`<div class="chat-panel-container"> return html`<div class="chat-panel-container">
<div class="chat-panel-title"> <div class="chat-panel-title">
@@ -294,7 +313,7 @@ export class PlaygroundChat extends SignalWatcher(
<ai-history-clear <ai-history-clear
.doc=${this.doc} .doc=${this.doc}
.session=${this.session} .session=${this.session}
.notification=${notification} .notificationService=${this.notificationService}
.onHistoryCleared=${this._updateHistory} .onHistoryCleared=${this._updateHistory}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
></ai-history-clear> ></ai-history-clear>
@@ -312,8 +331,11 @@ export class PlaygroundChat extends SignalWatcher(
.updateContext=${this.updateContext} .updateContext=${this.updateContext}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.networkSearchConfig=${this.networkSearchConfig} .networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig} .reasoningConfig=${this.reasoningConfig}
.messages=${this.messages}
></ai-chat-messages> ></ai-chat-messages>
<ai-chat-composer <ai-chat-composer
.host=${this.host} .host=${this.host}

View File

@@ -1,9 +1,11 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql'; import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std'; import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std'; import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType, Store } from '@blocksuite/affine/store'; import type { ExtensionType, Store } from '@blocksuite/affine/store';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import { css, html } from 'lit'; import { css, html } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
@@ -83,6 +85,12 @@ export class PlaygroundContent extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService; accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor affineThemeService!: AppThemeService;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@state() @state()
accessor sessions: CopilotChatHistoryFragment[] = []; accessor sessions: CopilotChatHistoryFragment[] = [];
@@ -336,6 +344,8 @@ export class PlaygroundContent extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig} .docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions} .extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService} .affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.addChat=${this.addChat} .addChat=${this.addChat}
></playground-chat> ></playground-chat>
</div> </div>

View File

@@ -1,17 +1,15 @@
import { createReactComponentFromLit } from '@affine/component'; import { createReactComponentFromLit } from '@affine/component';
import { getStoreManager } from '@affine/core/blocksuite/manager/store';
import { getViewManager } from '@affine/core/blocksuite/manager/view'; import { getViewManager } from '@affine/core/blocksuite/manager/view';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { PeekViewProvider } from '@blocksuite/affine/components/peek'; 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 { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
import type { ColorScheme } from '@blocksuite/affine/model';
import { import {
codeBlockWrapMiddleware, codeBlockWrapMiddleware,
defaultImageProxyMiddleware, defaultImageProxyMiddleware,
ImageProxyService, ImageProxyService,
} from '@blocksuite/affine/shared/adapters'; } from '@blocksuite/affine/shared/adapters';
import { ThemeProvider } from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { import {
BlockStdScope, BlockStdScope,
@@ -22,10 +20,10 @@ import {
import type { import type {
ExtensionType, ExtensionType,
Query, Query,
Schema,
Store, Store,
TransformerMiddleware, TransformerMiddleware,
} from '@blocksuite/affine/store'; } from '@blocksuite/affine/store';
import type { Signal } from '@preact/signals-core';
import { import {
darkCssVariablesV2, darkCssVariablesV2,
lightCssVariablesV2, lightCssVariablesV2,
@@ -103,6 +101,7 @@ export type TextRendererOptions = {
additionalMiddlewares?: TransformerMiddleware[]; additionalMiddlewares?: TransformerMiddleware[];
testId?: string; testId?: string;
affineFeatureFlagService?: FeatureFlagService; affineFeatureFlagService?: FeatureFlagService;
theme?: Signal<ColorScheme>;
}; };
// todo: refactor it for more general purpose usage instead of AI only? // 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 _doc: Store | null = null;
private _host: EditorHost | null = null;
private readonly _query: Query = { private readonly _query: Query = {
mode: 'strict', mode: 'strict',
match: [ match: [
@@ -243,7 +244,7 @@ export class TextRenderer extends SignalWatcher(
private _timer?: ReturnType<typeof setInterval> | null = null; private _timer?: ReturnType<typeof setInterval> | null = null;
private readonly _subscribeDocLinkClicked = () => { private readonly _subscribeDocLinkClicked = () => {
const refNodeSlots = this.host?.std.getOptional(RefNodeSlotsProvider); const refNodeSlots = this._host?.std.getOptional(RefNodeSlotsProvider);
if (!refNodeSlots) return; if (!refNodeSlots) return;
this.disposables.add( this.disposables.add(
refNodeSlots.docLinkClicked refNodeSlots.docLinkClicked
@@ -254,7 +255,7 @@ export class TextRenderer extends SignalWatcher(
) )
.subscribe(options => { .subscribe(options => {
// Open the doc in center peek // Open the doc in center peek
this.host?.std this._host?.std
.getOptional(PeekViewProvider) .getOptional(PeekViewProvider)
?.peek({ ?.peek({
docId: options.pageId, docId: options.pageId,
@@ -268,40 +269,27 @@ export class TextRenderer extends SignalWatcher(
if (this._answers.length > 0) { if (this._answers.length > 0) {
const latestAnswer = this._answers.pop(); const latestAnswer = this._answers.pop();
this._answers = []; this._answers = [];
const schema = this.schema ?? this.host?.std.store.schema; if (latestAnswer) {
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) {
const middlewares = [ const middlewares = [
defaultImageProxyMiddleware, defaultImageProxyMiddleware,
codeBlockWrapMiddleware(true), codeBlockWrapMiddleware(true),
...(this.options.additionalMiddlewares ?? []), ...(this.options.additionalMiddlewares ?? []),
]; ];
const affineFeatureFlagService = this.options.affineFeatureFlagService;
markDownToDoc( markDownToDoc(
provider,
schema,
latestAnswer, latestAnswer,
middlewares, middlewares,
affineFeatureFlagService this.options.affineFeatureFlagService
) )
.then(doc => { .then(doc => {
this.disposeDoc(); this.disposeDoc();
this._doc = doc.doc.getStore({ this._doc = doc.doc.getStore({
query: this._query, query: this._query,
}); });
this._host = new BlockStdScope({
store: this._doc,
extensions:
this.options.extensions ?? getCustomPageEditorBlockSpecs(),
}).render();
this.disposables.add(() => { this.disposables.add(() => {
doc.doc.removeStore({ query: this._query }); doc.doc.removeStore({ query: this._query });
}); });
@@ -309,14 +297,10 @@ export class TextRenderer extends SignalWatcher(
this.requestUpdate(); this.requestUpdate();
if (this.state !== 'generating') { if (this.state !== 'generating') {
this._doc.load(); this._doc.load();
// LinkPreviewService & ImageProxyService config should read from host settings const imageProxyService = this._host.std.get(ImageProxyService);
const imageProxyService = imageProxyService.setImageProxyURL(
this.host?.std.store.get(ImageProxyService); imageProxyService.imageProxyURL
if (imageProxyService) { );
this._doc
?.get(ImageProxyService)
.setImageProxyURL(imageProxyService.imageProxyURL);
}
this._clearTimer(); this._clearTimer();
} }
}) })
@@ -341,7 +325,6 @@ export class TextRenderer extends SignalWatcher(
private disposeDoc() { private disposeDoc() {
this._doc?.dispose(); this._doc?.dispose();
this._doc?.workspace.dispose();
} }
override disconnectedCallback() { override disconnectedCallback() {
@@ -355,22 +338,22 @@ export class TextRenderer extends SignalWatcher(
return nothing; return nothing;
} }
const { customHeading, testId } = this.options; const { customHeading, testId = 'ai-text-renderer' } = this.options;
const classes = classMap({ const classes = classMap({
'text-renderer-container': true, 'text-renderer-container': true,
'custom-heading': !!customHeading, 'custom-heading': !!customHeading,
}); });
const theme = this.host?.std.get(ThemeProvider).app$.value; const theme = this.options.theme?.value;
return html` return html`
<div class=${classes} data-testid=${testId} data-app-theme=${theme}> <div
class=${classes}
data-testid=${testId}
data-app-theme=${theme ?? 'light'}
>
${keyed( ${keyed(
this._doc, this._doc,
html`<div class="ai-answer-text-editor affine-page-viewport"> html`<div class="ai-answer-text-editor affine-page-viewport">
${new BlockStdScope({ ${this._host}
store: this._doc,
extensions:
this.options.extensions ?? getCustomPageEditorBlockSpecs(),
}).render()}
</div>` </div>`
)} )}
</div> </div>
@@ -416,12 +399,6 @@ export class TextRenderer extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor answer!: string; accessor answer!: string;
@property({ attribute: false })
accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor schema: Schema | null = null;
@property({ attribute: false }) @property({ attribute: false })
accessor options!: TextRendererOptions; accessor options!: TextRendererOptions;
@@ -429,14 +406,10 @@ export class TextRenderer extends SignalWatcher(
accessor state: AffineAIPanelState | undefined = undefined; accessor state: AffineAIPanelState | undefined = undefined;
} }
export const createTextRenderer = ( export const createTextRenderer = (options: TextRendererOptions) => {
host: EditorHost | null | undefined,
options: TextRendererOptions
) => {
return (answer: string, state?: AffineAIPanelState) => { return (answer: string, state?: AffineAIPanelState) => {
return html`<text-renderer return html`<text-renderer
contenteditable="false" contenteditable="false"
.host=${host}
.answer=${answer} .answer=${answer}
.state=${state} .state=${state}
.options=${options} .options=${options}

View File

@@ -468,6 +468,8 @@ export class AIChatBlockPeekView extends LitElement {
return html`<ai-loading></ai-loading>`; return html`<ai-loading></ai-loading>`;
} }
const notificationService = this.host.std.get(NotificationProvider);
return html`<div class=${messageClasses}> return html`<div class=${messageClasses}>
<ai-chat-block-message <ai-chat-block-message
.host=${host} .host=${host}
@@ -485,6 +487,7 @@ export class AIChatBlockPeekView extends LitElement {
.isLast=${isLastReply} .isLast=${isLastReply}
.messageId=${message.id ?? undefined} .messageId=${message.id ?? undefined}
.retry=${() => this.retry()} .retry=${() => this.retry()}
.notificationService=${notificationService}
></chat-copy-more>` ></chat-copy-more>`
: nothing} : nothing}
${shouldRenderActions ${shouldRenderActions
@@ -495,6 +498,7 @@ export class AIChatBlockPeekView extends LitElement {
.content=${markdown} .content=${markdown}
.messageId=${message.id ?? undefined} .messageId=${message.id ?? undefined}
.layoutDirection=${'horizontal'} .layoutDirection=${'horizontal'}
.notificationService=${notificationService}
></chat-action-list>` ></chat-action-list>`
: nothing} : nothing}
</div>`; </div>`;
@@ -569,7 +573,7 @@ export class AIChatBlockPeekView extends LitElement {
} = this; } = this;
const { messages: currentChatMessages } = chatContext; const { messages: currentChatMessages } = chatContext;
const notification = this.host.std.getOptional(NotificationProvider); const notificationService = this.host.std.get(NotificationProvider);
return html`<div class="ai-chat-block-peek-view-container"> return html`<div class="ai-chat-block-peek-view-container">
<div class="history-clear-container"> <div class="history-clear-container">
@@ -578,7 +582,7 @@ export class AIChatBlockPeekView extends LitElement {
.session=${this.forkSession} .session=${this.forkSession}
.onHistoryCleared=${this._onHistoryCleared} .onHistoryCleared=${this._onHistoryCleared}
.chatContextValue=${chatContext} .chatContextValue=${chatContext}
.notification=${notification} .notificationService=${notificationService}
></ai-history-clear> ></ai-history-clear>
</div> </div>
<div class="ai-chat-messages-container"> <div class="ai-chat-messages-container">

View File

@@ -2,6 +2,7 @@ import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onb
import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud'; import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud';
import type { GlobalDialogService } from '@affine/core/modules/dialogs'; import type { GlobalDialogService } from '@affine/core/modules/dialogs';
import { import {
type AddContextFileInput,
ContextCategories, ContextCategories,
type ContextWorkspaceEmbeddingStatus, type ContextWorkspaceEmbeddingStatus,
type getCopilotHistoriesQuery, 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 }) => { removeContextDoc: async (options: { contextId: string; docId: string }) => {
return client.removeContextDoc(options); return client.removeContextDoc(options);
}, },
addContextFile: async ( addContextFile: async (file: File, options: AddContextFileInput) => {
file: File,
options: { contextId: string; blobId: string }
) => {
return client.addContextFile(file, options); return client.addContextFile(file, options);
}, },
removeContextFile: async (options: { removeContextFile: async (options: {

View File

@@ -1,10 +1,13 @@
import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace'; import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
import { clipboardConfigs } from '@blocksuite/affine/foundation/clipboard';
import { defaultImageProxyMiddleware } from '@blocksuite/affine/shared/adapters'; import { defaultImageProxyMiddleware } from '@blocksuite/affine/shared/adapters';
import { replaceSelectedTextWithBlocksCommand } from '@blocksuite/affine/shared/commands'; import { replaceSelectedTextWithBlocksCommand } from '@blocksuite/affine/shared/commands';
import { isInsideEdgelessEditor } from '@blocksuite/affine/shared/utils'; import { isInsideEdgelessEditor } from '@blocksuite/affine/shared/utils';
import { import {
type BlockComponent, type BlockComponent,
BlockSelection, BlockSelection,
BlockStdScope,
Clipboard,
type EditorHost, type EditorHost,
SurfaceSelection, SurfaceSelection,
type TextSelection, type TextSelection,
@@ -185,27 +188,25 @@ export const replace = async (
}; };
export const copyTextAnswer = async (panel: AffineAIPanelWidget) => { export const copyTextAnswer = async (panel: AffineAIPanelWidget) => {
const host = panel.host;
if (!panel.answer) { if (!panel.answer) {
return false; return false;
} }
return copyText(host, panel.answer); return copyText(panel.answer);
}; };
export const copyText = async (host: EditorHost, text: string) => { export const copyText = async (text: string) => {
const previewDoc = await markDownToDoc( const previewDoc = await markDownToDoc(text, [defaultImageProxyMiddleware]);
host.std.store.provider,
host.std.store.schema,
text,
[defaultImageProxyMiddleware]
);
const models = previewDoc const models = previewDoc
.getBlocksByFlavour('affine:note') .getBlocksByFlavour('affine:note')
.map(b => b.model) .map(b => b.model)
.flatMap(model => model.children); .flatMap(model => model.children);
const slice = Slice.fromModels(previewDoc, models); 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.dispose();
previewDoc.workspace.dispose();
return true; return true;
}; };

View File

@@ -1,4 +1,3 @@
import type { ServiceProvider } from '@blocksuite/affine/global/di';
import { import {
DatabaseBlockModel, DatabaseBlockModel,
ImageBlockModel, ImageBlockModel,
@@ -19,10 +18,11 @@ import {
isInsideEdgelessEditor, isInsideEdgelessEditor,
matchModels, matchModels,
} from '@blocksuite/affine/shared/utils'; } 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 type { BlockModel, Store } from '@blocksuite/affine/store';
import { Slice, toDraftModel } 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 type { ChatContextValue } from '../components/ai-chat-content';
import { import {
getSelectedImagesAsBlobs, getSelectedImagesAsBlobs,
@@ -96,12 +96,13 @@ async function extractPageSelected(
} }
} }
export async function extractMarkdownFromDoc( export async function extractMarkdownFromDoc(doc: Store): Promise<string> {
doc: Store, const std = new BlockStdScope({
provider: ServiceProvider store: doc,
): Promise<string> { extensions: getStoreManager().config.init().value.get('store'),
});
const transformer = await getTransformer(doc); const transformer = await getTransformer(doc);
const adapter = new MarkdownAdapter(transformer, provider); const adapter = new MarkdownAdapter(transformer, std.provider);
const blockModels = getNoteBlockModels(doc); const blockModels = getNoteBlockModels(doc);
const textModels = blockModels.filter( const textModels = blockModels.filter(
model => !matchModels(model, [ImageBlockModel, DatabaseBlockModel]) model => !matchModels(model, [ImageBlockModel, DatabaseBlockModel])

View File

@@ -1,4 +1,5 @@
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std'; import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css, html, nothing, type TemplateResult } from 'lit'; import { css, html, nothing, type TemplateResult } from 'lit';
import { literal, unsafeStatic } from 'lit/static-html.js'; import { literal, unsafeStatic } from 'lit/static-html.js';
@@ -98,9 +99,9 @@ export class AffineBlockDiffWidgetForBlock extends WidgetComponent {
: html`<div class="ai-block-diff insert" data-diff-id=${diffId}> : html`<div class="ai-block-diff insert" data-diff-id=${diffId}>
<chat-content-rich-text <chat-content-rich-text
.text=${block.content} .text=${block.content}
.host=${this.host}
.state="finished" .state="finished"
.extensions=${this.userExtensions} .extensions=${this.userExtensions}
.theme=${this.host.std.get(ThemeProvider).app$}
></chat-content-rich-text> ></chat-content-rich-text>
<ai-block-diff-options <ai-block-diff-options
class="diff-options" class="diff-options"
@@ -132,9 +133,9 @@ export class AffineBlockDiffWidgetForBlock extends WidgetComponent {
<div class="ai-block-diff update" data-diff-id=${diffId}> <div class="ai-block-diff update" data-diff-id=${diffId}>
<chat-content-rich-text <chat-content-rich-text
.text=${content} .text=${content}
.host=${this.host}
.state="finished" .state="finished"
.extensions=${this.userExtensions} .extensions=${this.userExtensions}
.theme=${this.host.std.get(ThemeProvider).app$}
></chat-content-rich-text> ></chat-content-rich-text>
<ai-block-diff-options <ai-block-diff-options
class="diff-options" class="diff-options"

View File

@@ -15,10 +15,7 @@ import {
import { toDocSearchParams } from '@affine/core/modules/navigation/utils'; import { toDocSearchParams } from '@affine/core/modules/navigation/utils';
import { GlobalSessionStateService } from '@affine/core/modules/storage'; import { GlobalSessionStateService } from '@affine/core/modules/storage';
import { WorkbenchLink } from '@affine/core/modules/workbench'; import { WorkbenchLink } from '@affine/core/modules/workbench';
import { import { WorkspaceService } from '@affine/core/modules/workspace';
getAFFiNEWorkspaceSchema,
WorkspaceService,
} from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import track from '@affine/track'; import track from '@affine/track';
import type { import type {
@@ -338,7 +335,6 @@ export const LinkPreview = ({
<LitTextRenderer <LitTextRenderer
className={styles.linkPreviewRenderer} className={styles.linkPreviewRenderer}
answer={link.markdownPreview} answer={link.markdownPreview}
schema={getAFFiNEWorkspaceSchema()}
options={textRendererOptions} options={textRendererOptions}
/> />
)} )}

View File

@@ -1,6 +1,5 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag'; import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace'; import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
import type { ServiceProvider } from '@blocksuite/affine/global/di';
import { import {
defaultImageProxyMiddleware, defaultImageProxyMiddleware,
embedSyncedDocMiddleware, embedSyncedDocMiddleware,
@@ -11,6 +10,7 @@ import {
titleMiddleware, titleMiddleware,
} from '@blocksuite/affine/shared/adapters'; } from '@blocksuite/affine/shared/adapters';
import { import {
BlockStdScope,
type EditorHost, type EditorHost,
type TextRangePoint, type TextRangePoint,
TextSelection, TextSelection,
@@ -19,7 +19,6 @@ import type {
BlockModel, BlockModel,
BlockSnapshot, BlockSnapshot,
DraftModel, DraftModel,
Schema,
Slice, Slice,
SliceSnapshot, SliceSnapshot,
Store, Store,
@@ -27,6 +26,43 @@ import type {
} from '@blocksuite/affine/store'; } from '@blocksuite/affine/store';
import { toDraftModel, Transformer } from '@blocksuite/affine/store'; import { toDraftModel, Transformer } from '@blocksuite/affine/store';
import { Doc as YDoc } from 'yjs'; 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 = ( const updateSnapshotText = (
point: TextRangePoint, point: TextRangePoint,
snapshot: BlockSnapshot, snapshot: BlockSnapshot,
@@ -184,20 +220,14 @@ export async function replaceFromMarkdown(
} }
export async function markDownToDoc( export async function markDownToDoc(
provider: ServiceProvider,
schema: Schema,
answer: string, answer: string,
middlewares?: TransformerMiddleware[], middlewares?: TransformerMiddleware[],
affineFeatureFlagService?: FeatureFlagService affineFeatureFlagService?: FeatureFlagService
) { ) {
// Should not create a new doc in the original collection const { collection, std } = getMarkdownWorkspace(affineFeatureFlagService);
const collection = new WorkspaceImpl({
rootDoc: new YDoc({ guid: 'markdownToDoc' }),
featureFlagService: affineFeatureFlagService,
});
collection.meta.initialize();
const transformer = new Transformer({ const transformer = new Transformer({
schema, schema: std.store.schema,
blobCRUD: collection.blobSync, blobCRUD: collection.blobSync,
docCRUD: { docCRUD: {
create: (id: string) => collection.createDoc(id).getStore({ id }), create: (id: string) => collection.createDoc(id).getStore({ id }),
@@ -206,7 +236,7 @@ export async function markDownToDoc(
}, },
middlewares, middlewares,
}); });
const mdAdapter = new MarkdownAdapter(transformer, provider); const mdAdapter = new MarkdownAdapter(transformer, std.store.provider);
const doc = await mdAdapter.toDoc({ const doc = await mdAdapter.toDoc({
file: answer, file: answer,
assets: transformer.assetsManager, assets: transformer.assetsManager,

View File

@@ -1,4 +1,5 @@
import { import {
type ConfirmModalProps,
Input, Input,
type Notification, type Notification,
notify, notify,
@@ -7,127 +8,156 @@ import {
toReactNode, toReactNode,
type useConfirmModal, type useConfirmModal,
} from '@affine/component'; } 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<NotificationService['confirm']>[0]) => {
return new Promise<boolean>(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<NotificationService['prompt']>[0]) => {
return new Promise<string | null>(resolve => {
let value = autofill || '';
const description = (
<div>
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
<Input
autoSelect={true}
placeholder={placeholder}
defaultValue={value}
onChange={e => (value = e)}
/>
</div>
);
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<NotificationService['notify']>[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<NotificationService['notifyWithUndoAction']>[0]
) => {
this.notify(options);
};
}
export function patchNotificationService({ export function patchNotificationService({
closeConfirmModal, closeConfirmModal,
openConfirmModal, openConfirmModal,
}: ReturnType<typeof useConfirmModal>) { }: ReturnType<typeof useConfirmModal>) {
return NotificationExtension({ const notificationService = new NotificationServiceImpl(
confirm: async ({ title, message, confirmText, cancelText, abort }) => { closeConfirmModal,
return new Promise<boolean>(resolve => { openConfirmModal
openConfirmModal({ );
title: toReactNode(title), return NotificationExtension(notificationService);
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<string | null>(resolve => {
let value = autofill || '';
const description = (
<div>
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
<Input
autoSelect={true}
placeholder={placeholder}
defaultValue={value}
onChange={e => (value = e)}
/>
</div>
);
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);
});
},
});
} }

View File

@@ -1,11 +1,10 @@
import { observeResize } from '@affine/component'; import { observeResize, useConfirmModal } from '@affine/component';
import { CopilotClient } from '@affine/core/blocksuite/ai'; import { CopilotClient } from '@affine/core/blocksuite/ai';
import { AIChatContent } from '@affine/core/blocksuite/ai/components/ai-chat-content'; import { AIChatContent } from '@affine/core/blocksuite/ai/components/ai-chat-content';
import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar'; 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 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 { 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 { import {
EventSourceService, EventSourceService,
FetchService, FetchService,
@@ -13,6 +12,7 @@ import {
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { AppThemeService } from '@affine/core/modules/theme';
import { import {
ViewBody, ViewBody,
ViewHeader, ViewHeader,
@@ -21,8 +21,6 @@ import {
} from '@affine/core/modules/workbench'; } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace'; import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n'; 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 { type Signal, signal } from '@preact/signals-core';
import { useFramework, useService } from '@toeverything/infra'; import { useFramework, useService } from '@toeverything/infra';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -52,7 +50,6 @@ export const Component = () => {
const framework = useFramework(); const framework = useFramework();
const [isBodyProvided, setIsBodyProvided] = useState(false); const [isBodyProvided, setIsBodyProvided] = useState(false);
const [isHeaderProvided, setIsHeaderProvided] = useState(false); const [isHeaderProvided, setIsHeaderProvided] = useState(false);
const [host, setHost] = useState<EditorHost | null>(null);
const [chatContent, setChatContent] = useState<AIChatContent | null>(null); const [chatContent, setChatContent] = useState<AIChatContent | null>(null);
const [chatTool, setChatTool] = useState<AIChatToolbar | null>(null); const [chatTool, setChatTool] = useState<AIChatToolbar | null>(null);
const [currentSession, setCurrentSession] = useState<CopilotSession | null>( const [currentSession, setCurrentSession] = useState<CopilotSession | null>(
@@ -132,28 +129,11 @@ export const Component = () => {
[chatContent, chatTool, client, isOpeningSession, workspaceId] [chatContent, chatTool, client, isOpeningSession, workspaceId]
); );
// create a temp doc/host for ai-chat-content const confirmModal = useConfirmModal();
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();
};
}, []);
// init or update ai-chat-content // init or update ai-chat-content
useEffect(() => { useEffect(() => {
if (!isBodyProvided || !host) { if (!isBodyProvided) {
return; return;
} }
@@ -164,7 +144,6 @@ export const Component = () => {
} }
content.session = currentSession; content.session = currentSession;
content.host = host;
content.workspaceId = workspaceId; content.workspaceId = workspaceId;
content.docDisplayConfig = docDisplayConfig; content.docDisplayConfig = docDisplayConfig;
content.searchMenuConfig = searchMenuConfig; content.searchMenuConfig = searchMenuConfig;
@@ -174,6 +153,11 @@ export const Component = () => {
content.affineWorkspaceDialogService = framework.get( content.affineWorkspaceDialogService = framework.get(
WorkspaceDialogService WorkspaceDialogService
); );
content.affineThemeService = framework.get(AppThemeService);
content.notificationService = new NotificationServiceImpl(
confirmModal.closeConfirmModal,
confirmModal.openConfirmModal
);
if (!chatContent) { if (!chatContent) {
// initial values that won't change // initial values that won't change
@@ -190,12 +174,12 @@ export const Component = () => {
currentSession, currentSession,
docDisplayConfig, docDisplayConfig,
framework, framework,
host,
isBodyProvided, isBodyProvided,
networkSearchConfig, networkSearchConfig,
reasoningConfig, reasoningConfig,
searchMenuConfig, searchMenuConfig,
workspaceId, workspaceId,
confirmModal,
]); ]);
// init or update header ai-chat-toolbar // init or update header ai-chat-toolbar
@@ -213,6 +197,10 @@ export const Component = () => {
tool.workspaceId = workspaceId; tool.workspaceId = workspaceId;
tool.docDisplayConfig = docDisplayConfig; tool.docDisplayConfig = docDisplayConfig;
tool.onOpenSession = onOpenSession; tool.onOpenSession = onOpenSession;
tool.notificationService = new NotificationServiceImpl(
confirmModal.closeConfirmModal,
confirmModal.openConfirmModal
);
tool.onNewSession = () => { tool.onNewSession = () => {
if (!currentSession) return; if (!currentSession) return;
@@ -239,6 +227,7 @@ export const Component = () => {
onOpenSession, onOpenSession,
togglePin, togglePin,
workspaceId, workspaceId,
confirmModal,
]); ]);
const onChatContainerRef = useCallback((node: HTMLDivElement) => { const onChatContainerRef = useCallback((node: HTMLDivElement) => {

View File

@@ -1,8 +1,11 @@
import { useConfirmModal } from '@affine/component';
import { AIProvider, ChatPanel } from '@affine/core/blocksuite/ai'; import { AIProvider, ChatPanel } from '@affine/core/blocksuite/ai';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor'; 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 { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { AppThemeService } from '@affine/core/modules/theme';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewExtensionManagerIdentifier } from '@blocksuite/affine/ext-loader'; import { ViewExtensionManagerIdentifier } from '@blocksuite/affine/ext-loader';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference'; import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
@@ -51,6 +54,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
reasoningConfig, reasoningConfig,
playgroundConfig, playgroundConfig,
} = useAIChatConfig(); } = useAIChatConfig();
const confirmModal = useConfirmModal();
useEffect(() => { useEffect(() => {
if (!editor || !editor.host) return; if (!editor || !editor.host) return;
@@ -87,6 +91,11 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
); );
chatPanelRef.current.affineWorkbenchService = chatPanelRef.current.affineWorkbenchService =
framework.get(WorkbenchService); framework.get(WorkbenchService);
chatPanelRef.current.affineThemeService = framework.get(AppThemeService);
chatPanelRef.current.notificationService = new NotificationServiceImpl(
confirmModal.closeConfirmModal,
confirmModal.openConfirmModal
);
containerRef.current?.append(chatPanelRef.current); containerRef.current?.append(chatPanelRef.current);
} else { } else {
@@ -117,6 +126,7 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
searchMenuConfig, searchMenuConfig,
reasoningConfig, reasoningConfig,
playgroundConfig, playgroundConfig,
confirmModal,
]); ]);
const [autoResized, setAutoResized] = useState(false); const [autoResized, setAutoResized] = useState(false);

View File

@@ -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'; import { Entity, LiveData } from '@toeverything/infra';
export class AppTheme extends Entity { export class AppTheme extends Entity {
theme$ = new LiveData<string | undefined>(undefined); theme$ = new LiveData<string | undefined>(undefined);
themeSignal: Signal<ColorScheme>;
constructor() {
super();
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
this.theme$.map(theme =>
theme === 'dark' ? ColorScheme.Dark : ColorScheme.Light
),
ColorScheme.Light
);
this.themeSignal = signal;
this.disposables.push(cleanup);
}
} }

View File

@@ -26,7 +26,7 @@ function Container({
const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => { const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => {
return ( return (
<IconButton size="24" onClick={onToggle}> <IconButton size="24" onClick={onToggle} data-testid="right-sidebar-close">
<RightSidebarIcon /> <RightSidebarIcon />
</IconButton> </IconButton>
); );

View File

@@ -19,14 +19,14 @@ test.describe('AIBasic/Authority', () => {
page, page,
utils, 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')).toBeVisible();
await expect(page.getByTestId('ai-error-action-button')).toBeVisible(); await expect(page.getByTestId('ai-error-action-button')).toBeVisible();
}); });
test('should support login in error state', async ({ page, utils }) => { 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'); const loginButton = page.getByTestId('ai-error-action-button');
await loginButton.click(); await loginButton.click();

View File

@@ -13,12 +13,12 @@ test.describe('AIInsertion/AddToEdgelessAsNote', () => {
utils, utils,
}) => { }) => {
await utils.editor.focusToEditor(page); 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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -47,12 +47,12 @@ test.describe('AIInsertion/AddToEdgelessAsNote', () => {
await page.keyboard.press('Delete'); await page.keyboard.press('Delete');
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',

View File

@@ -22,12 +22,12 @@ test.describe('AIInsertion/Insert', () => {
await page.keyboard.insertText('World Block'); await page.keyboard.insertText('World Block');
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -60,12 +60,12 @@ test.describe('AIInsertion/Insert', () => {
await page.keyboard.insertText('World Block'); await page.keyboard.insertText('World Block');
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -101,12 +101,12 @@ test.describe('AIInsertion/Insert', () => {
await page.keyboard.insertText('World Block'); await page.keyboard.insertText('World Block');
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -139,12 +139,12 @@ test.describe('AIInsertion/Insert', () => {
await page.keyboard.insertText('World Block'); await page.keyboard.insertText('World Block');
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -173,12 +173,12 @@ test.describe('AIInsertion/Insert', () => {
await page.keyboard.press('Delete'); await page.keyboard.press('Delete');
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',

View File

@@ -13,12 +13,12 @@ test.describe('AIInsertion/SaveAsBlock', () => {
utils, utils,
}) => { }) => {
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -45,12 +45,12 @@ test.describe('AIInsertion/SaveAsBlock', () => {
await utils.editor.switchToEdgelessMode(page); await utils.editor.switchToEdgelessMode(page);
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',

View File

@@ -13,12 +13,12 @@ test.describe('AIInsertion/SaveAsDoc', () => {
utils, utils,
}) => { }) => {
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',
@@ -45,12 +45,12 @@ test.describe('AIInsertion/SaveAsDoc', () => {
await utils.editor.switchToEdgelessMode(page); await utils.editor.switchToEdgelessMode(page);
await utils.chatPanel.openChatPanel(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, [ await utils.chatPanel.waitForHistory(page, [
{ {
role: 'user', role: 'user',
content: 'Hello', content: 'Hello. Answer in 50 words.',
}, },
{ {
role: 'assistant', role: 'assistant',

View File

@@ -37,7 +37,7 @@ export class ChatPanelUtils {
} }
public static async closeChatPanel(page: Page) { public static async closeChatPanel(page: Page) {
await page.getByTestId('right-sidebar-toggle').click({ await page.getByTestId('right-sidebar-close').click({
delay: 200, delay: 200,
}); });
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeHidden(); await expect(page.getByTestId('sidebar-tab-content-chat')).toBeHidden();