mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user