feat(core): make editor host optional (#12990)

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 support for passing workspace and document identifiers directly
to chat components, enabling improved context handling in AI chat
features.

* **Bug Fixes**
* Improved null safety and error handling across AI chat components to
prevent issues when certain properties are missing.
* Enhanced defensive checks to avoid runtime errors related to missing
or undefined properties.

* **Refactor**
* Simplified and standardized property types and data flow in AI chat
components, reducing reliance on certain objects and making properties
optional where appropriate.
* Streamlined error messaging and tool integration by updating property
and parameter structures.
* Updated tool components to use image proxy services directly, removing
dependency on host objects.

* **Chores**
* Updated type definitions and interfaces for better flexibility and
maintainability.
  * Added new interfaces to clarify session creation parameters.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-03 08:49:07 +08:00
committed by GitHub
parent a21f1c943e
commit 3e03599d11
23 changed files with 161 additions and 118 deletions

View File

@@ -80,11 +80,11 @@ declare global {
retry?: boolean; retry?: boolean;
// action's context // action's context
docId: string; docId?: string;
workspaceId: string; workspaceId: string;
// internal context // internal context
host: EditorHost; host?: EditorHost;
models?: (BlockModel | GfxModel)[]; models?: (BlockModel | GfxModel)[];
control?: TrackerControl; control?: TrackerControl;
where?: TrackerWhere; where?: TrackerWhere;
@@ -142,6 +142,7 @@ declare global {
docs: AIDocContextOption[]; docs: AIDocContextOption[];
files: AIFileContextOption[]; files: AIFileContextOption[];
}; };
postfix?: (text: string) => string;
} }
interface TranslateOptions extends AITextActionOptions { interface TranslateOptions extends AITextActionOptions {
@@ -374,9 +375,9 @@ declare global {
}; };
interface CreateSessionOptions { interface CreateSessionOptions {
docId: string;
workspaceId: string;
promptName: PromptKey; promptName: PromptKey;
workspaceId: string;
docId?: string;
sessionId?: string; sessionId?: string;
retry?: boolean; retry?: boolean;
} }

View File

@@ -33,7 +33,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
`; `;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor item!: ChatMessage; accessor item!: ChatMessage;
@@ -99,7 +99,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
${streamObjects?.length ${streamObjects?.length
? this.renderStreamObjects(streamObjects) ? this.renderStreamObjects(streamObjects)
: this.renderRichText(content)} : this.renderRichText(content)}
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing} ${shouldRenderError ? AIChatErrorRenderer(error, host) : nothing}
${this.renderEditorActions()} ${this.renderEditorActions()}
`; `;
} }
@@ -152,9 +152,11 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
? mergeStreamContent(streamObjects) ? mergeStreamContent(streamObjects)
: content; : content;
const actions = isInsidePageEditor(host) const actions = host
? PageEditorActions ? isInsidePageEditor(host)
: EdgelessEditorActions; ? PageEditorActions
: EdgelessEditorActions
: null;
return html` return html`
<chat-copy-more <chat-copy-more
@@ -167,7 +169,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.withMargin=${true} .withMargin=${true}
.retry=${() => this.retry()} .retry=${() => this.retry()}
></chat-copy-more> ></chat-copy-more>
${isLast && !!markdown ${isLast && !!markdown && host
? html`<chat-action-list ? html`<chat-action-list
.actions=${actions} .actions=${actions}
.host=${host} .host=${host}

View File

@@ -83,7 +83,7 @@ export class ChatPanelChips extends SignalWatcher(
private _abortController: AbortController | null = null; private _abortController: AbortController | null = null;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor chips!: ChatChip[]; accessor chips!: ChatChip[];
@@ -407,6 +407,9 @@ 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 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,

View File

@@ -37,7 +37,7 @@ export class ChatPanelDocChip extends SignalWatcher(
accessor docDisplayConfig!: DocDisplayConfig; accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
private chipName = new Signal<string>(''); private chipName = new Signal<string>('');
@@ -103,6 +103,9 @@ 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) {

View File

@@ -51,11 +51,14 @@ export class AIChatComposer extends SignalWatcher(
`; `;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor workspaceId!: string; accessor workspaceId!: string;
@property({ attribute: false })
accessor docId: string | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor session!: CopilotSessionType | null | undefined; accessor session!: CopilotSessionType | null | undefined;
@@ -124,8 +127,10 @@ export class AIChatComposer extends SignalWatcher(
></chat-panel-chips> ></chat-panel-chips>
<ai-chat-input <ai-chat-input
.host=${this.host} .host=${this.host}
.chips=${this.chips} .workspaceId=${this.workspaceId}
.docId=${this.docId}
.session=${this.session} .session=${this.session}
.chips=${this.chips}
.createSession=${this.createSession} .createSession=${this.createSession}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext} .updateContext=${this.updateContext}
@@ -224,13 +229,12 @@ export class AIChatComposer extends SignalWatcher(
const fileChips: FileChip[] = await Promise.all( const fileChips: FileChip[] = await Promise.all(
files.map(async file => { files.map(async file => {
const blob = await this.host.store.blobSync.get(file.blobId);
return { return {
file: new File(blob ? [blob] : [], file.name), file: new File([], file.name),
blobId: file.blobId, blobId: file.blobId,
fileId: file.id, fileId: file.id,
state: blob ? file.status : 'failed', state: file.status,
tooltip: blob ? file.error : 'File not found in blob storage', tooltip: file.error,
createdAt: file.createdAt, createdAt: file.createdAt,
}; };
}) })
@@ -302,7 +306,7 @@ export class AIChatComposer extends SignalWatcher(
try { try {
await AIProvider.context?.pollEmbeddingStatus( await AIProvider.context?.pollEmbeddingStatus(
this.host.std.workspace.id, this.workspaceId,
(status: ContextWorkspaceEmbeddingStatus) => { (status: ContextWorkspaceEmbeddingStatus) => {
if (!status) { if (!status) {
this.embeddingCompleted = false; this.embeddingCompleted = false;

View File

@@ -72,10 +72,10 @@ export class AIChatContent extends SignalWatcher(
`; `;
@property({ attribute: false }) @property({ attribute: false })
accessor chatTitle!: TemplateResult<1>; accessor chatTitle: TemplateResult<1> | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor session!: CopilotSessionType | null | undefined; accessor session!: CopilotSessionType | null | undefined;
@@ -288,6 +288,8 @@ export class AIChatContent extends SignalWatcher(
<ai-chat-messages <ai-chat-messages
${ref(this.chatMessagesRef)} ${ref(this.chatMessagesRef)}
.host=${this.host} .host=${this.host}
.workspaceId=${this.workspaceId}
.docId=${this.docId}
.session=${this.session} .session=${this.session}
.createSession=${this.createSession} .createSession=${this.createSession}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
@@ -302,6 +304,7 @@ export class AIChatContent extends SignalWatcher(
<ai-chat-composer <ai-chat-composer
.host=${this.host} .host=${this.host}
.workspaceId=${this.workspaceId} .workspaceId=${this.workspaceId}
.docId=${this.docId}
.session=${this.session} .session=${this.session}
.createSession=${this.createSession} .createSession=${this.createSession}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}

View File

@@ -281,7 +281,13 @@ export class AIChatInput extends SignalWatcher(
`; `;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor workspaceId!: string;
@property({ attribute: false })
accessor docId: string | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor session!: CopilotSessionType | null | undefined; accessor session!: CopilotSessionType | null | undefined;
@@ -596,10 +602,9 @@ export class AIChatInput extends SignalWatcher(
sessionId, sessionId,
input: userInput, input: userInput,
contexts, contexts,
docId: this.host.store.id, docId: this.docId,
attachments: images, attachments: images,
workspaceId: this.host.store.workspace.id, workspaceId: this.workspaceId,
host: this.host,
stream: true, stream: true,
signal: abortController.signal, signal: abortController.signal,
isRootSession: this.isRootSession, isRootSession: this.isRootSession,
@@ -681,8 +686,8 @@ export class AIChatInput extends SignalWatcher(
const last = messages[messages.length - 1] as ChatMessage; const last = messages[messages.length - 1] as ChatMessage;
if (!last.id) { if (!last.id) {
const historyIds = await AIProvider.histories.ids( const historyIds = await AIProvider.histories.ids(
this.host.store.workspace.id, this.workspaceId,
this.host.store.id, this.docId,
{ sessionId } { sessionId }
); );
if (!historyIds || !historyIds[0]) return; if (!historyIds || !historyIds[0]) return;

View File

@@ -141,7 +141,13 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
accessor avatarUrl = ''; accessor avatarUrl = '';
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor workspaceId!: string;
@property({ attribute: false })
accessor docId: string | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor isHistoryLoading!: boolean; accessor isHistoryLoading!: boolean;
@@ -305,7 +311,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
.retry=${() => this.retry()} .retry=${() => this.retry()}
.width=${this.width} .width=${this.width}
></chat-message-assistant>`; ></chat-message-assistant>`;
} else if (isChatAction(item)) { } else if (isChatAction(item) && this.host) {
return html`<chat-message-action return html`<chat-message-action
.host=${this.host} .host=${this.host}
.item=${item} .item=${item}
@@ -330,7 +336,6 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
const { disposables } = this; const { disposables } = this;
const docModeService = this.host.std.get(DocModeProvider);
Promise.resolve(AIProvider.userInfo) Promise.resolve(AIProvider.userInfo)
.then(res => { .then(res => {
@@ -351,17 +356,25 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
} }
}) })
); );
disposables.add(
this.host.selection.slots.changed.subscribe(() => { const selection$ = this.host?.selection.slots.changed;
this._selectionValue = this.host.selection.value; if (selection$) {
}) disposables.add(
); selection$.subscribe(() => {
disposables.add( this._selectionValue = this.host?.selection.value ?? [];
docModeService.onPrimaryModeChange( })
() => this.requestUpdate(), );
this.host.store.id }
)
); const docModeService = this.host?.std.get(DocModeProvider);
if (docModeService && this.docId) {
disposables.add(
docModeService.onPrimaryModeChange(
() => this.requestUpdate(),
this.docId
)
);
}
} }
protected override updated(_changedProperties: PropertyValues) { protected override updated(_changedProperties: PropertyValues) {
@@ -400,13 +413,11 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
abortController, abortController,
}); });
const { store } = this.host;
const stream = await AIProvider.actions.chat({ const stream = await AIProvider.actions.chat({
sessionId, sessionId,
retry: true, retry: true,
docId: store.id, docId: this.docId,
workspaceId: store.workspace.id, workspaceId: this.workspaceId,
host: this.host,
stream: true, stream: true,
signal: abortController.signal, signal: abortController.signal,
where: 'chat-panel', where: 'chat-panel',

View File

@@ -19,7 +19,7 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
accessor session!: CopilotSessionType | null | undefined; accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor doc!: Store; accessor doc!: Store;
@@ -52,18 +52,19 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
return; return;
} }
const sessionId = this.session.id; const sessionId = this.session.id;
const notification = this.host.std.getOptional(NotificationProvider); const notification = this.host?.std.getOptional(NotificationProvider);
if (!notification) return;
try { try {
if ( const confirm = notification
await notification.confirm({ ? await 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) {
const actionIds = this.chatContextValue.messages const actionIds = this.chatContextValue.messages
.filter(item => 'sessionId' in item) .filter(item => 'sessionId' in item)
.map(item => item.sessionId); .map(item => item.sessionId);
@@ -72,11 +73,11 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
this.doc.id, this.doc.id,
[...(sessionId ? [sessionId] : []), ...(actionIds || [])] [...(sessionId ? [sessionId] : []), ...(actionIds || [])]
); );
notification.toast('History cleared'); notification?.toast('History cleared');
this.onHistoryCleared?.(); this.onHistoryCleared?.();
} }
} catch { } catch {
notification.toast('Failed to clear history'); notification?.toast('Failed to clear history');
} }
}; };

View File

@@ -10,7 +10,7 @@ import { createTextRenderer } from '../../components/text-renderer';
export class ChatContentRichText extends WithDisposable(ShadowlessElement) { export class ChatContentRichText extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor text!: string; accessor text!: string;

View File

@@ -1,5 +1,6 @@
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 { 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';
@@ -26,7 +27,7 @@ export class ChatContentStreamObjects extends WithDisposable(
accessor answer!: StreamObject[]; accessor answer!: StreamObject[];
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor state: AffineAIPanelState = 'finished'; accessor state: AffineAIPanelState = 'finished';
@@ -44,32 +45,28 @@ export class ChatContentStreamObjects extends WithDisposable(
if (streamObject.type !== 'tool-call') { if (streamObject.type !== 'tool-call') {
return nothing; return nothing;
} }
const imageProxyService = this.host?.store.get(ImageProxyService);
switch (streamObject.toolName) { switch (streamObject.toolName) {
case 'web_crawl_exa': case 'web_crawl_exa':
return html` return html`
<web-crawl-tool <web-crawl-tool
.data=${streamObject} .data=${streamObject}
.host=${this.host}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-crawl-tool> ></web-crawl-tool>
`; `;
case 'web_search_exa': case 'web_search_exa':
return html` return html`
<web-search-tool <web-search-tool
.data=${streamObject} .data=${streamObject}
.host=${this.host}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-search-tool> ></web-search-tool>
`; `;
default: { default: {
const name = streamObject.toolName + ' tool calling'; const name = streamObject.toolName + ' tool calling';
return html` return html`
<tool-call-card <tool-call-card .name=${name} .width=${this.width}></tool-call-card>
.name=${name}
.host=${this.host}
.width=${this.width}
></tool-call-card>
`; `;
} }
} }
@@ -79,22 +76,22 @@ 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);
switch (streamObject.toolName) { switch (streamObject.toolName) {
case 'web_crawl_exa': case 'web_crawl_exa':
return html` return html`
<web-crawl-tool <web-crawl-tool
.data=${streamObject} .data=${streamObject}
.host=${this.host}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-crawl-tool> ></web-crawl-tool>
`; `;
case 'web_search_exa': case 'web_search_exa':
return html` return html`
<web-search-tool <web-search-tool
.data=${streamObject} .data=${streamObject}
.host=${this.host}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></web-search-tool> ></web-search-tool>
`; `;
default: { default: {
@@ -102,8 +99,8 @@ export class ChatContentStreamObjects extends WithDisposable(
return html` return html`
<tool-result-card <tool-result-card
.name=${name} .name=${name}
.host=${this.host}
.width=${this.width} .width=${this.width}
.imageProxyService=${imageProxyService}
></tool-result-card> ></tool-result-card>
`; `;
} }

View File

@@ -83,10 +83,10 @@ export class AIScrollableTextRenderer extends WithDisposable(
accessor answer!: string; accessor answer!: string;
@property({ attribute: false }) @property({ attribute: false })
accessor host: EditorHost | null = null; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor state: AffineAIPanelState | undefined = undefined; accessor state: AffineAIPanelState | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor textRendererOptions!: TextRendererOptions; accessor textRendererOptions!: TextRendererOptions;

View File

@@ -1,7 +1,7 @@
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { ImageProxyService } from '@blocksuite/affine/shared/adapters'; import { type ImageProxyService } from '@blocksuite/affine/shared/adapters';
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 { 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';
@@ -190,9 +190,6 @@ export class ToolResultCard extends SignalWatcher(
} }
`; `;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false }) @property({ attribute: false })
accessor name: string = 'Tool result'; accessor name: string = 'Tool result';
@@ -208,6 +205,9 @@ 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;
@@ -276,9 +276,11 @@ export class ToolResultCard extends SignalWatcher(
if (!icon) { if (!icon) {
return nothing; return nothing;
} }
const imageProxyService = this.host.store.get(ImageProxyService);
if (typeof icon === 'string') { if (typeof icon === 'string') {
return html` <img src=${imageProxyService.buildUrl(icon)} /> `; if (this.imageProxyService) {
return html`<img src=${this.imageProxyService.buildUrl(icon)} />`;
}
return html`<img src=${icon} />`;
} }
return html`${icon}`; return html`${icon}`;
} }

View File

@@ -1,5 +1,6 @@
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; import type { ImageProxyService } from '@blocksuite/affine/shared/adapters';
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';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
@@ -37,10 +38,10 @@ export class WebCrawlTool extends WithDisposable(ShadowlessElement) {
accessor data!: WebCrawlToolCall | WebCrawlToolResult; accessor data!: WebCrawlToolCall | WebCrawlToolResult;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor width: Signal<number | undefined> | undefined; accessor imageProxyService: ImageProxyService | null | undefined;
renderToolCall() { renderToolCall() {
return html` return html`
@@ -61,7 +62,6 @@ export class WebCrawlTool extends WithDisposable(ShadowlessElement) {
const { favicon, title, content } = result[0]; const { favicon, title, content } = result[0];
return html` return html`
<tool-result-card <tool-result-card
.host=${this.host}
.name=${'The reading is complete, and this webpage has been read'} .name=${'The reading is complete, and this webpage has been read'}
.icon=${WebIcon()} .icon=${WebIcon()}
.footerIcons=${favicon ? [favicon] : []} .footerIcons=${favicon ? [favicon] : []}
@@ -73,6 +73,7 @@ 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,6 @@
import { WithDisposable } from '@blocksuite/affine/global/lit'; import { WithDisposable } from '@blocksuite/affine/global/lit';
import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std'; import type { ImageProxyService } from '@blocksuite/affine/shared/adapters';
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';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
@@ -37,10 +38,10 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
accessor data!: WebSearchToolCall | WebSearchToolResult; accessor data!: WebSearchToolCall | WebSearchToolResult;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor width: Signal<number | undefined> | undefined; accessor imageProxyService: ImageProxyService | null | undefined;
renderToolCall() { renderToolCall() {
return html` return html`
@@ -69,12 +70,12 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
return html` return html`
<tool-result-card <tool-result-card
.host=${this.host}
.name=${'The search is complete, and these webpages have been searched'} .name=${'The search is complete, and these webpages have been searched'}
.icon=${WebIcon()} .icon=${WebIcon()}
.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

@@ -82,7 +82,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
`; `;
private get _selectionValue() { private get _selectionValue() {
return this.host.selection.value; return this.host?.selection.value ?? [];
} }
private get _currentTextSelection(): TextSelection | undefined { private get _currentTextSelection(): TextSelection | undefined {
@@ -105,7 +105,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
private _morePopper: ReturnType<typeof createButtonPopper> | null = null; private _morePopper: ReturnType<typeof createButtonPopper> | null = null;
@property({ attribute: false }) @property({ attribute: false })
accessor host!: EditorHost; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor actions: ChatAction[] = []; accessor actions: ChatAction[] = [];
@@ -136,7 +136,8 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
} }
private readonly _notifySuccess = (title: string) => { private readonly _notifySuccess = (title: string) => {
const notificationService = this.host.std.getOptional(NotificationProvider); const notificationService =
this.host?.std.getOptional(NotificationProvider);
notificationService?.notify({ notificationService?.notify({
title: title, title: title,
accent: 'success', accent: 'success',
@@ -174,7 +175,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
} }
</style> </style>
<div class="copy-more"> <div class="copy-more">
${content ${content && host
? html`<div ? html`<div
class="button copy" class="button copy"
@click=${async () => { @click=${async () => {
@@ -199,19 +200,19 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
<affine-tooltip .autoShift=${true}>Retry</affine-tooltip> <affine-tooltip .autoShift=${true}>Retry</affine-tooltip>
</div>` </div>`
: nothing} : nothing}
${isLast ${!isLast && host
? nothing ? html`<div
: html`<div
class="button more" class="button more"
data-testid="action-more-button" data-testid="action-more-button"
@click=${this._toggle} @click=${this._toggle}
> >
${MoreHorizontalIcon({ width: '20px', height: '20px' })} ${MoreHorizontalIcon({ width: '20px', height: '20px' })}
</div> `} </div> `
: nothing}
</div> </div>
<div class="more-menu"> <div class="more-menu">
${this._showMoreMenu ${this._showMoreMenu && host
? repeat( ? repeat(
actions.filter(action => action.showWhen(host)), actions.filter(action => action.showWhen(host)),
action => action.title, action => action.title,

View File

@@ -298,6 +298,8 @@ export class PlaygroundChat extends SignalWatcher(
<ai-chat-messages <ai-chat-messages
${ref(this._chatMessagesRef)} ${ref(this._chatMessagesRef)}
.host=${this.host} .host=${this.host}
.workspaceId=${this.doc.workspace.id}
.docId=${this.doc.id}
.isHistoryLoading=${this.isLoading} .isHistoryLoading=${this.isLoading}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}
.session=${this.session} .session=${this.session}
@@ -311,6 +313,7 @@ export class PlaygroundChat extends SignalWatcher(
<ai-chat-composer <ai-chat-composer
.host=${this.host} .host=${this.host}
.workspaceId=${this.doc.workspace.id} .workspaceId=${this.doc.workspace.id}
.docId=${this.doc.id}
.session=${this.session} .session=${this.session}
.createSession=${this._createSession} .createSession=${this._createSession}
.chatContextValue=${this.chatContextValue} .chatContextValue=${this.chatContextValue}

View File

@@ -415,7 +415,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
accessor answer!: string; accessor answer!: string;
@property({ attribute: false }) @property({ attribute: false })
accessor host: EditorHost | null = null; accessor host: EditorHost | null | undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor schema: Schema | null = null; accessor schema: Schema | null = null;
@@ -428,7 +428,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
} }
export const createTextRenderer = ( export const createTextRenderer = (
host: EditorHost, host: EditorHost | null | undefined,
options: TextRendererOptions options: TextRendererOptions
) => { ) => {
return (answer: string, state?: AffineAIPanelState) => { return (answer: string, state?: AffineAIPanelState) => {

View File

@@ -186,7 +186,7 @@ export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
accessor testId = 'ai-error'; accessor testId = 'ai-error';
} }
const PaymentRequiredErrorRenderer = (host: EditorHost) => html` const PaymentRequiredErrorRenderer = (host?: EditorHost | null) => html`
<ai-error-wrapper <ai-error-wrapper
.text=${"You've reached the current usage cap for AFFiNE AI. You can subscribe to AFFiNE AI(with free 7-day-trial) to continue the AI experience!"} .text=${"You've reached the current usage cap for AFFiNE AI. You can subscribe to AFFiNE AI(with free 7-day-trial) to continue the AI experience!"}
.actionText=${'Upgrade'} .actionText=${'Upgrade'}
@@ -194,7 +194,7 @@ const PaymentRequiredErrorRenderer = (host: EditorHost) => html`
></ai-error-wrapper> ></ai-error-wrapper>
`; `;
const LoginRequiredErrorRenderer = (host: EditorHost) => html` const LoginRequiredErrorRenderer = (host?: EditorHost | null) => html`
<ai-error-wrapper <ai-error-wrapper
.text=${'You need to login to AFFiNE Cloud to continue using AFFiNE AI.'} .text=${'You need to login to AFFiNE Cloud to continue using AFFiNE AI.'}
.actionText=${'Login'} .actionText=${'Login'}
@@ -227,7 +227,7 @@ const GeneralErrorRenderer = (props: ErrorProps = {}) => {
></ai-error-wrapper>`; ></ai-error-wrapper>`;
}; };
export function AIChatErrorRenderer(host: EditorHost, error: AIError) { export function AIChatErrorRenderer(error: AIError, host?: EditorHost | null) {
if (error instanceof PaymentRequiredError) { if (error instanceof PaymentRequiredError) {
return PaymentRequiredErrorRenderer(host); return PaymentRequiredErrorRenderer(host);
} else if (error instanceof UnauthorizedError) { } else if (error instanceof UnauthorizedError) {

View File

@@ -471,7 +471,7 @@ export class AIChatBlockPeekView extends LitElement {
.message=${message} .message=${message}
.textRendererOptions=${this._textRendererOptions} .textRendererOptions=${this._textRendererOptions}
></ai-chat-block-message> ></ai-chat-block-message>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing} ${shouldRenderError ? AIChatErrorRenderer(error, host) : nothing}
${shouldRenderCopyMore ${shouldRenderCopyMore
? html` <chat-copy-more ? html` <chat-copy-more
.host=${host} .host=${host}
@@ -590,6 +590,7 @@ export class AIChatBlockPeekView extends LitElement {
<ai-chat-composer <ai-chat-composer
.host=${host} .host=${host}
.workspaceId=${this.rootWorkspaceId} .workspaceId=${this.rootWorkspaceId}
.docId=${this.rootDocId}
.session=${this.forkSession ?? this.session} .session=${this.forkSession ?? this.session}
.createSession=${this.createForkSession} .createSession=${this.createForkSession}
.chatContextValue=${chatContext} .chatContextValue=${chatContext}

View File

@@ -139,8 +139,8 @@ export class AIProvider {
template: string; template: string;
mode: 'page' | 'edgeless'; mode: 'page' | 'edgeless';
}>(), }>(),
requestLogin: new Subject<{ host: EditorHost }>(), requestLogin: new Subject<{ host?: EditorHost | null }>(),
requestUpgradePlan: new Subject<{ host: EditorHost }>(), requestUpgradePlan: new Subject<{ host?: EditorHost | null }>(),
// stream of AI actions triggered by users // stream of AI actions triggered by users
actions: new Subject<{ actions: new Subject<{
action: keyof BlockSuitePresets.AIActions; action: keyof BlockSuitePresets.AIActions;

View File

@@ -42,24 +42,26 @@ const processTypeToPromptName = new Map<string, PromptKey>(
}) })
); );
interface CreateSessionOptions {
promptName: PromptKey;
workspaceId: string;
docId?: string;
sessionId?: string;
retry?: boolean;
}
export function setupAIProvider( export function setupAIProvider(
client: CopilotClient, client: CopilotClient,
globalDialogService: GlobalDialogService, globalDialogService: GlobalDialogService,
authService: AuthService authService: AuthService
) { ) {
async function createSession({ async function createSession({
promptName,
workspaceId, workspaceId,
docId, docId,
promptName,
sessionId, sessionId,
retry, retry,
}: { }: CreateSessionOptions) {
workspaceId: string;
docId: string;
promptName: PromptKey;
sessionId?: string;
retry?: boolean;
}) {
if (sessionId) return sessionId; if (sessionId) return sessionId;
if (retry) return AIProvider.LAST_ACTION_SESSIONID; if (retry) return AIProvider.LAST_ACTION_SESSIONID;

View File

@@ -16,7 +16,7 @@ type AIActionEventName =
| 'AI result accepted'; | 'AI result accepted';
type AIActionEventProperties = { type AIActionEventProperties = {
page: 'doc' | 'edgeless'; page: 'doc' | 'edgeless' | 'unknown';
segment: segment:
| 'AI action panel' | 'AI action panel'
| 'right side bar' | 'right side bar'
@@ -58,7 +58,7 @@ type AIActionEventProperties = {
| 'other'; | 'other';
category: string; category: string;
other: Record<string, unknown>; other: Record<string, unknown>;
docId: string; docId?: string;
workspaceId: string; workspaceId: string;
}; };
@@ -231,7 +231,9 @@ const toTrackedOptions = (
if (!eventName) return null; if (!eventName) return null;
const pageMode = inferPageMode(event.options.host); const pageMode = event.options.host
? inferPageMode(event.options.host)
: 'unknown';
const otherProperties = omit(event.options, defaultActionOptions); const otherProperties = omit(event.options, defaultActionOptions);
const type = inferObjectType(event); const type = inferObjectType(event);
const segment = inferSegment(event); const segment = inferSegment(event);