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

View File

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

View File

@@ -83,7 +83,7 @@ export class ChatPanelChips extends SignalWatcher(
private _abortController: AbortController | null = null;
@property({ attribute: false })
accessor host!: EditorHost;
accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor chips!: ChatChip[];
@@ -407,6 +407,9 @@ export class ChatPanelChips extends SignalWatcher(
if (!contextId || !AIProvider.context) {
throw new Error('Context not found');
}
if (!this.host) {
throw new Error('Host not found');
}
const blobId = await this.host.store.blobSync.set(chip.file);
const contextFile = await AIProvider.context.addContextFile(chip.file, {
contextId,

View File

@@ -37,7 +37,7 @@ export class ChatPanelDocChip extends SignalWatcher(
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor host!: EditorHost;
accessor host: EditorHost | null | undefined;
private chipName = new Signal<string>('');
@@ -103,6 +103,9 @@ export class ChatPanelDocChip extends SignalWatcher(
};
private readonly processDocChip = async () => {
if (!this.host) {
return;
}
try {
const doc = this.docDisplayConfig.getDoc(this.chip.docId);
if (!doc) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor host!: EditorHost;
accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor doc!: Store;
@@ -52,18 +52,19 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
return;
}
const sessionId = this.session.id;
const notification = this.host.std.getOptional(NotificationProvider);
if (!notification) return;
const notification = this.host?.std.getOptional(NotificationProvider);
try {
if (
await notification.confirm({
title: 'Clear History',
message:
'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.',
confirmText: 'Confirm',
cancelText: 'Cancel',
})
) {
const confirm = notification
? await notification.confirm({
title: 'Clear History',
message:
'Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.',
confirmText: 'Confirm',
cancelText: 'Cancel',
})
: true;
if (confirm) {
const actionIds = this.chatContextValue.messages
.filter(item => 'sessionId' in item)
.map(item => item.sessionId);
@@ -72,11 +73,11 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
this.doc.id,
[...(sessionId ? [sessionId] : []), ...(actionIds || [])]
);
notification.toast('History cleared');
notification?.toast('History cleared');
this.onHistoryCleared?.();
}
} 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) {
@property({ attribute: false })
accessor host!: EditorHost;
accessor host: EditorHost | null | undefined;
@property({ attribute: false })
accessor text!: string;

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
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 { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
import { ToggleDownIcon, ToolIcon } from '@blocksuite/icons/lit';
import { type Signal } from '@preact/signals-core';
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 })
accessor name: string = 'Tool result';
@@ -208,6 +205,9 @@ export class ToolResultCard extends SignalWatcher(
@property({ attribute: false })
accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor imageProxyService: ImageProxyService | null | undefined;
@state()
private accessor isCollapsed = true;
@@ -276,9 +276,11 @@ export class ToolResultCard extends SignalWatcher(
if (!icon) {
return nothing;
}
const imageProxyService = this.host.store.get(ImageProxyService);
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}`;
}

View File

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

View File

@@ -1,5 +1,6 @@
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 type { Signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
@@ -37,10 +38,10 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
accessor data!: WebSearchToolCall | WebSearchToolResult;
@property({ attribute: false })
accessor host!: EditorHost;
accessor width: Signal<number | undefined> | undefined;
@property({ attribute: false })
accessor width: Signal<number | undefined> | undefined;
accessor imageProxyService: ImageProxyService | null | undefined;
renderToolCall() {
return html`
@@ -69,12 +70,12 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
return html`
<tool-result-card
.host=${this.host}
.name=${'The search is complete, and these webpages have been searched'}
.icon=${WebIcon()}
.footerIcons=${footerIcons}
.results=${results}
.width=${this.width}
.imageProxyService=${this.imageProxyService}
></tool-result-card>
`;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -139,8 +139,8 @@ export class AIProvider {
template: string;
mode: 'page' | 'edgeless';
}>(),
requestLogin: new Subject<{ host: EditorHost }>(),
requestUpgradePlan: new Subject<{ host: EditorHost }>(),
requestLogin: new Subject<{ host?: EditorHost | null }>(),
requestUpgradePlan: new Subject<{ host?: EditorHost | null }>(),
// stream of AI actions triggered by users
actions: new Subject<{
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(
client: CopilotClient,
globalDialogService: GlobalDialogService,
authService: AuthService
) {
async function createSession({
promptName,
workspaceId,
docId,
promptName,
sessionId,
retry,
}: {
workspaceId: string;
docId: string;
promptName: PromptKey;
sessionId?: string;
retry?: boolean;
}) {
}: CreateSessionOptions) {
if (sessionId) return sessionId;
if (retry) return AIProvider.LAST_ACTION_SESSIONID;

View File

@@ -16,7 +16,7 @@ type AIActionEventName =
| 'AI result accepted';
type AIActionEventProperties = {
page: 'doc' | 'edgeless';
page: 'doc' | 'edgeless' | 'unknown';
segment:
| 'AI action panel'
| 'right side bar'
@@ -58,7 +58,7 @@ type AIActionEventProperties = {
| 'other';
category: string;
other: Record<string, unknown>;
docId: string;
docId?: string;
workspaceId: string;
};
@@ -231,7 +231,9 @@ const toTrackedOptions = (
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 type = inferObjectType(event);
const segment = inferSegment(event);