feat(core): remove chat-panel component's dependency on doc (#12975)

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

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

* **New Features**
* Introduced a unified AI chat content component to manage and display
chat interactions.
* Added new chat block message components for improved chat message
rendering.

* **Refactor**
* Simplified and unified session management across all AI chat
components, now passing full session objects instead of session IDs.
* Updated component and property names for clarity and consistency
(e.g., chat message and block message components).
* Consolidated chat history and actions retrieval for a more streamlined
chat experience.
* Removed redundant session ID getters and replaced them with direct
session object usage.
* Streamlined chat panel and composer components by removing internal
message and context state management.

* **Bug Fixes**
* Improved handling of chat session state and loading, reducing
redundant state properties.
* Enhanced event handling to prevent errors when chat parameters are
missing.

* **Tests**
* Removed outdated chat clearing test cases to align with new chat state
management.

* **Chores**
* Updated import paths and reorganized module exports for better
maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-01 19:20:20 +08:00
committed by GitHub
parent d49a069351
commit 6e9487a9e1
28 changed files with 669 additions and 649 deletions

View File

@@ -1,4 +1,3 @@
import { ChatHistoryOrder } from '@affine/graphql';
import { EdgelessCRUDIdentifier } from '@blocksuite/affine/blocks/surface';
import {
Bound,
@@ -69,14 +68,15 @@ export type ChatAction = {
export async function queryHistoryMessages(
workspaceId: string,
docId: string,
forkSessionId: string
forkSessionId: string,
docId?: string
) {
// Get fork session messages
const histories = await AIProvider.histories?.chats(workspaceId, docId, {
sessionId: forkSessionId,
messageOrder: ChatHistoryOrder.asc,
});
const histories = await AIProvider.histories?.chats(
workspaceId,
forkSessionId,
docId
);
if (!histories || !histories.length) {
return [];
@@ -117,8 +117,8 @@ export async function constructRootChatBlockMessages(
const userInfo = await AIProvider.userInfo;
const forkMessages = (await queryHistoryMessages(
doc.workspace.id,
doc.id,
forkSessionId
forkSessionId,
doc.id
)) as ChatMessage[];
return constructUserInfoWithMessages(forkMessages, userInfo);
}

View File

@@ -1,5 +1,4 @@
import type {
ChatHistoryOrder,
ContextMatchedDocChunk,
ContextMatchedFileChunk,
ContextWorkspaceEmbeddingStatus,
@@ -400,15 +399,12 @@ declare global {
// non chat histories
actions: (
workspaceId: string,
docId?: string
docId: string
) => Promise<AIHistory[] | undefined>;
chats: (
workspaceId: string,
docId?: string,
options?: {
sessionId?: string;
messageOrder?: ChatHistoryOrder;
}
sessionId: string,
docId?: string
) => Promise<AIHistory[] | undefined>;
cleanup: (
workspaceId: string,

View File

@@ -49,12 +49,12 @@ export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
return html`<div class="affine-ai-chat-block-container">
<div class="ai-chat-messages-container">
<ai-chat-messages
<ai-chat-block-messages
.host=${this.host}
.messages=${messages}
.textRendererOptions=${this._textRendererOptions}
.withMask=${true}
></ai-chat-messages>
></ai-chat-block-messages>
</div>
<div class="ai-chat-block-button">
${ChatWithAIIcon} <span>AI chat block</span>

View File

@@ -11,7 +11,7 @@ import {
} from '../../../components/ai-chat-messages';
import { UserInfoTemplate } from './user-info';
export class AIChatMessage extends LitElement {
export class AIChatBlockMessage extends LitElement {
static override styles = css`
.ai-chat-message {
display: flex;
@@ -99,7 +99,7 @@ export class AIChatMessage extends LitElement {
accessor textRendererOptions: TextRendererOptions = {};
}
export class AIChatMessages extends LitElement {
export class AIChatBlockMessages extends LitElement {
static override styles = css`
:host {
width: 100%;
@@ -123,11 +123,11 @@ export class AIChatMessages extends LitElement {
message => message.id || message.createdAt,
message => {
return html`
<ai-chat-message
<ai-chat-block-message
.host=${this.host}
.textRendererOptions=${this.textRendererOptions}
.message=${message}
></ai-chat-message>
></ai-chat-block-message>
`;
}
)}
@@ -146,7 +146,7 @@ export class AIChatMessages extends LitElement {
declare global {
interface HTMLElementTagNameMap {
'ai-chat-message': AIChatMessage;
'ai-chat-messages': AIChatMessages;
'ai-chat-block-message': AIChatBlockMessage;
'ai-chat-block-messages': AIChatBlockMessages;
}
}

View File

@@ -1,5 +1,3 @@
import './chat-panel-messages';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
@@ -12,9 +10,8 @@ import { CenterPeekIcon } from '@blocksuite/icons/lit';
import { type Signal, signal } from '@preact/signals-core';
import { css, html, nothing, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
import { keyed } from 'lit/directives/keyed.js';
import { styleMap } from 'lit/directives/style-map.js';
import { throttle } from 'lodash-es';
import type {
DocDisplayConfig,
@@ -25,31 +22,9 @@ import type {
AIPlaygroundConfig,
AIReasoningConfig,
} from '../components/ai-chat-input';
import {
type ChatAction,
type ChatMessage,
type HistoryMessage,
} from '../components/ai-chat-messages';
import { createPlaygroundModal } from '../components/playground/modal';
import { AIProvider } from '../provider';
import { extractSelectedContent } from '../utils/extract';
import {
getSelectedImagesAsBlobs,
getSelectedTextContent,
} from '../utils/selection-utils';
import type { AppSidebarConfig } from './chat-config';
import type { ChatContextValue } from './chat-context';
import type { ChatPanelMessages } from './chat-panel-messages';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '',
images: [],
abortController: null,
messages: [],
status: 'idle',
error: null,
markdown: '',
};
export class ChatPanel extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -58,24 +33,10 @@ export class ChatPanel extends SignalWatcher(
chat-panel {
width: 100%;
user-select: text;
}
.chat-panel-container {
display: flex;
flex-direction: column;
height: 100%;
}
.chat-panel-title {
background: var(--affine-background-primary-color);
position: relative;
padding: 8px 0px;
width: 100%;
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1;
.chat-panel-container {
height: 100%;
}
.chat-panel-title-text {
font-size: 14px;
@@ -83,153 +44,40 @@ export class ChatPanel extends SignalWatcher(
color: var(--affine-text-secondary-color);
}
svg {
width: 18px;
height: 18px;
color: var(--affine-text-secondary-color);
.chat-panel-playground {
cursor: pointer;
padding: 2px;
margin-left: 8px;
margin-right: auto;
display: flex;
justify-content: center;
align-items: center;
}
.chat-panel-playground:hover svg {
color: ${unsafeCSSVarV2('icon/activated')};
}
}
chat-panel-messages {
flex: 1;
overflow-y: hidden;
}
.chat-panel-hints {
margin: 0 4px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--affine-border-color);
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.chat-panel-hints :first-child {
color: var(--affine-text-primary-color);
}
.chat-panel-hints :nth-child(2) {
color: var(--affine-text-secondary-color);
}
.chat-panel-playground {
cursor: pointer;
padding: 2px;
margin-left: 8px;
margin-right: auto;
display: flex;
justify-content: center;
align-items: center;
}
.chat-panel-playground:hover svg {
color: ${unsafeCSSVarV2('icon/activated')};
}
`;
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
createRef<ChatPanelMessages>();
// request counter to track the latest request
private _updateHistoryCounter = 0;
private _wheelTriggered = false;
private readonly _updateHistory = async () => {
const { doc } = this;
const currentRequest = ++this._updateHistoryCounter;
const [histories, actions] = await Promise.all([
AIProvider.histories?.chats(doc.workspace.id, doc.id),
AIProvider.histories?.actions(doc.workspace.id, doc.id),
]);
// Check if this is still the latest request
if (currentRequest !== this._updateHistoryCounter) {
return;
}
const chatActions = (actions || []) as ChatAction[];
const messages: HistoryMessage[] = chatActions;
const sessionId = await this._getSessionId();
const history = histories?.find(history => history.sessionId === sessionId);
if (history) {
const chatMessages = (history.messages || []) as ChatMessage[];
messages.push(...chatMessages);
}
this.chatContextValue = {
...this.chatContextValue,
messages: messages.sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
),
};
this._scrollToEnd();
};
private readonly _updateEmbeddingProgress = (
count: Record<ContextEmbedStatus, number>
) => {
const total = count.finished + count.processing + count.failed;
this.embeddingProgress = [count.finished, total];
};
private readonly _getSessionId = async () => {
if (this.session) {
return this.session.id;
}
const sessions = (
(await AIProvider.session?.getSessions(
this.doc.workspace.id,
this.doc.id,
{ action: false }
)) || []
).filter(session => !session.parentSessionId);
this.session = sessions.at(-1);
return this.session?.id;
};
private readonly _createSessionId = async () => {
if (this.session) {
return this.session.id;
}
const sessionId = await AIProvider.session?.createSession({
docId: this.doc.id,
workspaceId: this.doc.workspace.id,
promptName: 'Chat With AFFiNE AI',
});
if (sessionId) {
this.session = await AIProvider.session?.getSession(
this.doc.workspace.id,
sessionId
);
}
return sessionId;
};
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor doc!: Store;
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false })
accessor playgroundConfig!: AIPlaygroundConfig;
@property({ attribute: false })
accessor appSidebarConfig!: AppSidebarConfig;
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig;
@@ -246,56 +94,74 @@ export class ChatPanel extends SignalWatcher(
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@state()
accessor isLoading = false;
@state()
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
accessor session: CopilotSessionType | null | undefined;
@state()
accessor embeddingProgress: [number, number] = [0, 0];
@state()
accessor session: CopilotSessionType | undefined = undefined;
private isSidebarOpen: Signal<boolean | undefined> = signal(false);
private _isInitialized = false;
private sidebarWidth: Signal<number | undefined> = signal(undefined);
private _isSidebarOpen: Signal<boolean | undefined> = signal(false);
private _sidebarWidth: Signal<number | undefined> = signal(undefined);
private readonly _scrollToEnd = () => {
if (!this._wheelTriggered) {
this._chatMessagesRef.value?.scrollToEnd();
private readonly initSession = async () => {
if (this.session) {
return this.session;
}
const sessions = (
(await AIProvider.session?.getSessions(
this.doc.workspace.id,
this.doc.id,
{ action: false }
)) || []
).filter(session => !session.parentSessionId);
const session = sessions.at(-1);
this.session = session ?? null;
return session;
};
private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 600);
private readonly createSession = async () => {
if (this.session) {
return this.session;
}
const sessionId = await AIProvider.session?.createSession({
docId: this.doc.id,
workspaceId: this.doc.workspace.id,
promptName: 'Chat With AFFiNE AI',
});
if (sessionId) {
const session = await AIProvider.session?.getSession(
this.doc.workspace.id,
sessionId
);
this.session = session ?? null;
}
return this.session;
};
private readonly _initPanel = async () => {
private readonly initPanel = async () => {
try {
if (!this._isSidebarOpen.value) return;
if (this.isLoading) return;
const userId = (await AIProvider.userInfo)?.id;
if (!userId) return;
this.isLoading = true;
await this._updateHistory();
this.isLoading = false;
this._isInitialized = true;
if (!this.isSidebarOpen.value) {
return;
}
await this.initSession();
} catch (error) {
console.error(error);
}
};
private readonly _resetPanel = () => {
private readonly resetPanel = () => {
this.session = undefined;
this.chatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
this.isLoading = false;
this._isInitialized = false;
this.embeddingProgress = [0, 0];
};
private readonly _openPlayground = () => {
private readonly updateEmbeddingProgress = (
count: Record<ContextEmbedStatus, number>
) => {
const total = count.finished + count.processing + count.failed;
this.embeddingProgress = [count.finished, total];
};
private readonly openPlayground = () => {
const playgroundContent = html`
<playground-content
.host=${this.host}
@@ -314,48 +180,10 @@ export class ChatPanel extends SignalWatcher(
createPlaygroundModal(playgroundContent, 'AI Playground');
};
protected override willUpdate(_changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) {
this._resetPanel();
requestAnimationFrame(async () => {
await this._initPanel();
});
}
}
protected override updated(_changedProperties: PropertyValues) {
if (this.chatContextValue.status === 'loading') {
// reset the wheel triggered flag when the status is loading
this._wheelTriggered = false;
}
if (
_changedProperties.has('chatContextValue') &&
(this.chatContextValue.status === 'loading' ||
this.chatContextValue.status === 'error' ||
this.chatContextValue.status === 'success')
) {
setTimeout(this._scrollToEnd, 500);
}
if (
_changedProperties.has('chatContextValue') &&
this.chatContextValue.status === 'transmitting'
) {
this._throttledScrollToEnd();
}
}
protected override firstUpdated(): void {
const chatMessages = this._chatMessagesRef.value;
if (chatMessages) {
chatMessages.updateComplete
.then(() => {
chatMessages.getScrollContainer()?.addEventListener('wheel', () => {
this._wheelTriggered = true;
});
})
.catch(console.error);
protected override updated(changedProperties: PropertyValues) {
if (changedProperties.has('doc')) {
this.resetPanel();
this.initPanel().catch(console.error);
}
}
@@ -363,135 +191,80 @@ export class ChatPanel extends SignalWatcher(
super.connectedCallback();
if (!this.doc) throw new Error('doc is required');
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
const { status } = this.chatContextValue;
if (
event === 'finished' &&
(status === 'idle' || status === 'success')
) {
this._updateHistory().catch(console.error);
}
})
);
this._disposables.add(
AIProvider.slots.userInfo.subscribe(() => {
this._initPanel().catch(console.error);
})
);
this._disposables.add(
AIProvider.slots.requestOpenWithChat.subscribe(({ host }) => {
if (this.host === host) {
extractSelectedContent(host)
.then(context => {
if (!context) return;
this.updateContext(context);
})
.catch(console.error);
}
this.resetPanel();
this.initPanel().catch(console.error);
})
);
const isOpen = this.appSidebarConfig.isOpen();
this._isSidebarOpen = isOpen.signal;
this.isSidebarOpen = isOpen.signal;
this._disposables.add(isOpen.cleanup);
const width = this.appSidebarConfig.getWidth();
this._sidebarWidth = width.signal;
this.sidebarWidth = width.signal;
this._disposables.add(width.cleanup);
this._disposables.add(
this._isSidebarOpen.subscribe(isOpen => {
if (isOpen && !this._isInitialized) {
this._initPanel().catch(console.error);
this.isSidebarOpen.subscribe(() => {
if (this.session === undefined) {
this.initPanel().catch(console.error);
}
})
);
}
updateContext = (context: Partial<ChatContextValue>) => {
this.chatContextValue = { ...this.chatContextValue, ...context };
};
continueInChat = async () => {
const text = await getSelectedTextContent(this.host, 'plain-text');
const markdown = await getSelectedTextContent(this.host, 'markdown');
const images = await getSelectedImagesAsBlobs(this.host);
this.updateContext({
quote: text,
markdown,
images,
});
};
override render() {
const width = this._sidebarWidth.value || 0;
const isInitialized = this.session !== undefined;
if (!isInitialized) {
return nothing;
}
const width = this.sidebarWidth.value || 0;
const style = styleMap({
padding: width > 540 ? '8px 24px 0 24px' : '8px 12px 0 12px',
});
const [done, total] = this.embeddingProgress;
const isEmbedding = total > 0 && done < total;
const title = html`
<div class="chat-panel-title-text">
${isEmbedding
? html`<span data-testid="chat-panel-embedding-progress"
>Embedding ${done}/${total}</span
>`
: 'AFFiNE AI'}
</div>
${this.playgroundConfig.visible.value
? html`
<div class="chat-panel-playground" @click=${this.openPlayground}>
${CenterPeekIcon()}
</div>
`
: nothing}
`;
return html`<div class="chat-panel-container" style=${style}>
<div class="chat-panel-title">
<div class="chat-panel-title-text">
${isEmbedding
? html`<span data-testid="chat-panel-embedding-progress"
>Embedding ${done}/${total}</span
>`
: 'AFFiNE AI'}
</div>
${this.playgroundConfig.visible.value
? html`
<div class="chat-panel-playground" @click=${this._openPlayground}>
${CenterPeekIcon()}
</div>
`
: nothing}
<ai-history-clear
${keyed(
this.doc.id,
html`<ai-chat-content
.chatTitle=${title}
.host=${this.host}
.doc=${this.doc}
.getSessionId=${this._getSessionId}
.onHistoryCleared=${this._updateHistory}
.chatContextValue=${this.chatContextValue}
></ai-history-clear>
</div>
<chat-panel-messages
${ref(this._chatMessagesRef)}
.chatContextValue=${this.chatContextValue}
.getSessionId=${this._getSessionId}
.createSessionId=${this._createSessionId}
.updateContext=${this.updateContext}
.host=${this.host}
.isLoading=${this.isLoading}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.panelWidth=${this._sidebarWidth}
></chat-panel-messages>
<ai-chat-composer
.host=${this.host}
.doc=${this.doc}
.session=${this.session}
.getSessionId=${this._getSessionId}
.createSessionId=${this._createSessionId}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
.isVisible=${this._isSidebarOpen}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig}
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',
}}
.panelWidth=${this._sidebarWidth}
></ai-chat-composer>
.session=${this.session}
.createSession=${this.createSession}
.workspaceId=${this.doc.workspace.id}
.docId=${this.doc.id}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.searchMenuConfig=${this.searchMenuConfig}
.docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.updateEmbeddingProgress=${this.updateEmbeddingProgress}
.width=${this.sidebarWidth}
></ai-chat-content>`
)}
</div>`;
}
}

View File

@@ -1,4 +1,5 @@
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { CopilotSessionType } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
@@ -53,7 +54,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor retry!: () => void;
@@ -62,7 +63,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
accessor testId = 'chat-message-assistant';
@property({ attribute: false })
accessor panelWidth!: Signal<number | undefined>;
accessor width: Signal<number | undefined> | undefined;
get state() {
const { isLast, status } = this;
@@ -117,7 +118,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.answer=${answer}
.host=${this.host}
.state=${this.state}
.width=${this.panelWidth}
.width=${this.width}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
></chat-content-stream-objects>`;
@@ -134,7 +135,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
}
private renderEditorActions() {
const { item, isLast, status } = this;
const { item, isLast, status, host, session } = this;
if (!isChatMessage(item) || item.role !== 'assistant') return nothing;
@@ -146,7 +147,6 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
)
return nothing;
const { host } = this;
const { content, streamObjects, id: messageId } = item;
const markdown = streamObjects?.length
? mergeStreamContent(streamObjects)
@@ -159,10 +159,10 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
return html`
<chat-copy-more
.host=${host}
.session=${session}
.actions=${actions}
.content=${markdown}
.isLast=${isLast}
.getSessionId=${this.getSessionId}
.messageId=${messageId}
.withMargin=${true}
.retry=${() => this.retry()}
@@ -171,8 +171,8 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
? html`<chat-action-list
.actions=${actions}
.host=${host}
.session=${session}
.content=${markdown}
.getSessionId=${this.getSessionId}
.messageId=${messageId ?? undefined}
.withMargin=${true}
></chat-action-list>`

View File

@@ -12,9 +12,7 @@ import type {
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
import type { Store } from '@blocksuite/affine/store';
import { type Signal, signal } from '@preact/signals-core';
import { css, html, type PropertyValues } from 'lit';
import { css, html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { AIProvider } from '../../provider';
@@ -56,16 +54,13 @@ export class AIChatComposer extends SignalWatcher(
accessor host!: EditorHost;
@property({ attribute: false })
accessor doc!: Store;
accessor workspaceId!: string;
@property({ attribute: false })
accessor session!: CopilotSessionType | undefined;
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor createSessionId!: () => Promise<string | undefined>;
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false })
accessor chatContextValue!: AIChatInputContext;
@@ -73,9 +68,6 @@ export class AIChatComposer extends SignalWatcher(
@property({ attribute: false })
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
@property({ attribute: false })
accessor isVisible: Signal<boolean | undefined> = signal(false);
@property({ attribute: false })
accessor updateEmbeddingProgress!: (
count: Record<ContextEmbedStatus, number>
@@ -102,9 +94,6 @@ export class AIChatComposer extends SignalWatcher(
@property({ attribute: false })
accessor portalContainer: HTMLElement | null = null;
@property({ attribute: false })
accessor panelWidth: Signal<number | undefined> = signal(undefined);
@property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@@ -114,10 +103,6 @@ export class AIChatComposer extends SignalWatcher(
@state()
accessor embeddingCompleted = false;
private _isInitialized = false;
private _isLoading = false;
private _contextId: string | undefined = undefined;
private _pollAbortController: AbortController | null = null;
@@ -141,9 +126,7 @@ export class AIChatComposer extends SignalWatcher(
.host=${this.host}
.chips=${this.chips}
.session=${this.session}
.getSessionId=${this.getSessionId}
.createSessionId=${this.createSessionId}
.getContextId=${this._getContextId}
.createSession=${this.createSession}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.networkSearchConfig=${this.networkSearchConfig}
@@ -151,7 +134,6 @@ export class AIChatComposer extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.onChatSuccess=${this.onChatSuccess}
.trackOptions=${this.trackOptions}
.panelWidth=${this.panelWidth}
.addImages=${this.addImages}
></ai-chat-input>
<div class="chat-panel-footer">
@@ -173,25 +155,7 @@ export class AIChatComposer extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
if (!this.doc) throw new Error('doc is required');
this._disposables.add(
AIProvider.slots.userInfo.subscribe(() => {
this._initComposer().catch(console.error);
})
);
this._disposables.add(
this.isVisible.subscribe(isVisible => {
if (isVisible && !this._isInitialized) {
this._initComposer().catch(console.error);
}
if (!isVisible) {
this._abortPoll();
this._abortPollEmbeddingStatus();
}
})
);
this._initComposer().catch(console.error);
}
override disconnectedCallback() {
@@ -200,25 +164,16 @@ export class AIChatComposer extends SignalWatcher(
this._abortPollEmbeddingStatus();
}
protected override willUpdate(_changedProperties: PropertyValues) {
if (_changedProperties.has('doc')) {
this._resetComposer();
requestAnimationFrame(async () => {
await this._initComposer();
});
}
}
private readonly _getContextId = async () => {
if (this._contextId) {
return this._contextId;
}
const sessionId = await this.getSessionId();
const sessionId = this.session?.id;
if (!sessionId) return;
const contextId = await AIProvider.context?.getContextId(
this.doc.workspace.id,
this.workspaceId,
sessionId
);
this._contextId = contextId;
@@ -230,11 +185,11 @@ export class AIChatComposer extends SignalWatcher(
return this._contextId;
}
const sessionId = await this.createSessionId();
const sessionId = (await this.createSession())?.id;
if (!sessionId) return;
this._contextId = await AIProvider.context?.createContext(
this.doc.workspace.id,
this.workspaceId,
sessionId
);
return this._contextId;
@@ -242,7 +197,7 @@ export class AIChatComposer extends SignalWatcher(
private readonly _initChips = async () => {
// context not initialized
const sessionId = await this.getSessionId();
const sessionId = this.session?.id;
const contextId = await this._getContextId();
if (!sessionId || !contextId) {
return;
@@ -255,7 +210,7 @@ export class AIChatComposer extends SignalWatcher(
tags = [],
collections = [],
} = (await AIProvider.context?.getContextDocsAndFiles(
this.doc.workspace.id,
this.workspaceId,
sessionId,
contextId
)) || {};
@@ -319,7 +274,7 @@ export class AIChatComposer extends SignalWatcher(
};
private readonly _pollContextDocsAndFiles = async () => {
const sessionId = await this.getSessionId();
const sessionId = this.session?.id;
const contextId = await this._getContextId();
if (!sessionId || !contextId || !AIProvider.context) {
return;
@@ -330,7 +285,7 @@ export class AIChatComposer extends SignalWatcher(
}
this._pollAbortController = new AbortController();
await AIProvider.context.pollContextDocsAndFiles(
this.doc.workspace.id,
this.workspaceId,
sessionId,
contextId,
this._onPoll,
@@ -436,13 +391,9 @@ export class AIChatComposer extends SignalWatcher(
};
private readonly _initComposer = async () => {
if (!this.isVisible.value) return;
if (this._isLoading) return;
const userId = (await AIProvider.userInfo)?.id;
if (!userId) return;
if (!userId || !this.session) return;
this._isLoading = true;
await this._initChips();
const needPoll = this.chips.some(
chip =>
@@ -452,16 +403,5 @@ export class AIChatComposer extends SignalWatcher(
await this._pollContextDocsAndFiles();
}
await this._pollEmbeddingStatus();
this._isLoading = false;
this._isInitialized = true;
};
private readonly _resetComposer = () => {
this._abortPoll();
this._abortPollEmbeddingStatus();
this.chips = [];
this._contextId = undefined;
this._isLoading = false;
this._isInitialized = false;
};
}

View File

@@ -0,0 +1,321 @@
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store';
import { type Signal } from '@preact/signals-core';
import { css, html, type PropertyValues, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
import { throttle } from 'lodash-es';
import { type AIChatParams, AIProvider } from '../../provider/ai-provider';
import { extractSelectedContent } from '../../utils/extract';
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
import type {
AINetworkSearchConfig,
AIReasoningConfig,
} from '../ai-chat-input';
import {
type AIChatMessages,
type ChatAction,
type ChatMessage,
type HistoryMessage,
isChatMessage,
} from '../ai-chat-messages';
import type { ChatContextValue } from './type';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '',
images: [],
abortController: null,
messages: [],
status: 'idle',
error: null,
markdown: '',
};
export class AIChatContent extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
ai-chat-content {
display: flex;
flex-direction: column;
height: 100%;
.ai-chat-title {
background: var(--affine-background-primary-color);
position: relative;
padding: 8px 0px;
width: 100%;
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1;
svg {
width: 18px;
height: 18px;
color: var(--affine-text-secondary-color);
}
}
ai-chat-messages {
flex: 1;
overflow-y: hidden;
}
}
`;
@property({ attribute: false })
accessor chatTitle!: TemplateResult<1>;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false })
accessor workspaceId!: string;
@property({ attribute: false })
accessor docId: string | undefined;
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor extensions!: ExtensionType[];
@property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor updateEmbeddingProgress!: (
count: Record<ContextEmbedStatus, number>
) => void;
@property({ attribute: false })
accessor width: Signal<number | undefined> | undefined;
@state()
accessor chatContextValue: ChatContextValue = DEFAULT_CHAT_CONTEXT_VALUE;
@state()
accessor isHistoryLoading = false;
private readonly chatMessagesRef: Ref<AIChatMessages> =
createRef<AIChatMessages>();
// request counter to track the latest request
private updateHistoryCounter = 0;
private wheelTriggered = false;
private readonly updateHistory = async () => {
const currentRequest = ++this.updateHistoryCounter;
if (!AIProvider.histories) {
return;
}
const sessionId = this.session?.id;
const [histories, actions] = await Promise.all([
sessionId
? AIProvider.histories.chats(this.workspaceId, sessionId, this.docId)
: Promise.resolve([]),
this.docId
? AIProvider.histories.actions(this.workspaceId, this.docId)
: Promise.resolve([]),
]);
// Check if this is still the latest request
if (currentRequest !== this.updateHistoryCounter) {
return;
}
const messages: HistoryMessage[] = this.chatContextValue.messages.slice();
const chatActions = (actions || []) as ChatAction[];
messages.push(...chatActions);
const chatMessages = (histories?.[0]?.messages || []) as ChatMessage[];
messages.push(...chatMessages);
this.updateContext({
messages: messages.sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
),
});
this.scrollToEnd();
};
private readonly updateActions = async () => {
if (!this.docId || !AIProvider.histories) {
return;
}
const actions = await AIProvider.histories.actions(
this.workspaceId,
this.docId
);
if (actions && actions.length) {
const chatMessages = this.chatContextValue.messages.filter(message =>
isChatMessage(message)
);
const chatActions = actions as ChatAction[];
const messages: HistoryMessage[] = [...chatMessages, ...chatActions];
this.updateContext({
messages: messages.sort(
(a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
),
});
}
};
private readonly updateContext = (context: Partial<ChatContextValue>) => {
this.chatContextValue = { ...this.chatContextValue, ...context };
};
private readonly scrollToEnd = () => {
if (!this.wheelTriggered) {
this.chatMessagesRef.value?.scrollToEnd();
}
};
private readonly _throttledScrollToEnd = throttle(this.scrollToEnd, 600);
private readonly initChatContent = async () => {
this.isHistoryLoading = true;
await this.updateHistory();
this.isHistoryLoading = false;
};
protected override firstUpdated(): void {
const chatMessages = this.chatMessagesRef.value;
if (chatMessages) {
chatMessages.updateComplete
.then(() => {
chatMessages.getScrollContainer()?.addEventListener('wheel', () => {
this.wheelTriggered = true;
});
})
.catch(console.error);
}
}
protected override updated(changedProperties: PropertyValues) {
if (this.chatContextValue.status === 'loading') {
// reset the wheel triggered flag when the status is loading
this.wheelTriggered = false;
}
if (
changedProperties.has('chatContextValue') &&
(this.chatContextValue.status === 'loading' ||
this.chatContextValue.status === 'error' ||
this.chatContextValue.status === 'success')
) {
setTimeout(this.scrollToEnd, 500);
}
if (
changedProperties.has('chatContextValue') &&
this.chatContextValue.status === 'transmitting'
) {
this._throttledScrollToEnd();
}
}
override connectedCallback() {
super.connectedCallback();
this.initChatContent().catch(console.error);
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
const { status } = this.chatContextValue;
if (
event === 'finished' &&
(status === 'idle' || status === 'success')
) {
this.updateActions().catch(console.error);
}
})
);
this._disposables.add(
AIProvider.slots.requestOpenWithChat.subscribe(
(params: AIChatParams | null) => {
if (!params) {
return;
}
if (this.host === params.host) {
extractSelectedContent(params.host)
.then(context => {
if (!context) return;
this.updateContext(context);
})
.catch(console.error);
}
}
)
);
}
override render() {
return html` <div class="ai-chat-title">${this.chatTitle}</div>
<ai-chat-messages
${ref(this.chatMessagesRef)}
.host=${this.host}
.session=${this.session}
.createSession=${this.createSession}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.isHistoryLoading=${this.isHistoryLoading}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.width=${this.width}
></ai-chat-messages>
<ai-chat-composer
.host=${this.host}
.workspaceId=${this.workspaceId}
.session=${this.session}
.createSession=${this.createSession}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.updateEmbeddingProgress=${this.updateEmbeddingProgress}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',
}}
></ai-chat-composer>`;
}
}

View File

@@ -0,0 +1,2 @@
export * from './ai-chat-content';
export * from './type';

View File

@@ -1,8 +1,5 @@
import type {
ChatStatus,
HistoryMessage,
} from '../components/ai-chat-messages';
import type { AIError } from '../provider';
import type { AIError } from '../../provider';
import type { ChatStatus, HistoryMessage } from '../ai-chat-messages';
export type ChatContextValue = {
// history messages of the chat

View File

@@ -6,14 +6,13 @@ import { openFilesWith } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
import { ArrowUpBigIcon, CloseIcon, ImageIcon } from '@blocksuite/icons/lit';
import { type Signal, signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { ChatAbortIcon } from '../../_common/icons';
import { type AIError, AIProvider } from '../../provider';
import { type AIError, AIProvider, type AISendParams } from '../../provider';
import { reportResponse } from '../../utils/action-reporter';
import { readBlobAsURL } from '../../utils/image';
import { mergeStreamObjects } from '../../utils/stream-objects';
@@ -285,7 +284,7 @@ export class AIChatInput extends SignalWatcher(
accessor host!: EditorHost;
@property({ attribute: false })
accessor session!: CopilotSessionType | undefined;
accessor session!: CopilotSessionType | null | undefined;
@query('image-preview-grid')
accessor imagePreviewGrid: HTMLDivElement | null = null;
@@ -309,13 +308,7 @@ export class AIChatInput extends SignalWatcher(
accessor chips: ChatChip[] = [];
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor createSessionId!: () => Promise<string | undefined>;
@property({ attribute: false })
accessor getContextId!: () => Promise<string | undefined>;
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false })
accessor updateContext!: (context: Partial<AIChatInputContext>) => void;
@@ -341,9 +334,6 @@ export class AIChatInput extends SignalWatcher(
@property({ attribute: 'data-testid', reflect: true })
accessor testId = 'chat-panel-input-container';
@property({ attribute: false })
accessor panelWidth: Signal<number | undefined> = signal(undefined);
@property({ attribute: false })
accessor addImages!: (images: File[]) => void;
@@ -366,9 +356,15 @@ export class AIChatInput extends SignalWatcher(
super.connectedCallback();
this._disposables.add(
AIProvider.slots.requestSendWithChat.subscribe(
({ input, context, host }) => {
(params: AISendParams | null) => {
if (!params) {
return;
}
const { input, context, host } = params;
if (this.host === host) {
context && this.updateContext(context);
if (context) {
this.updateContext(context);
}
setTimeout(() => {
this.send(input).catch(console.error);
}, 0);
@@ -591,7 +587,7 @@ export class AIChatInput extends SignalWatcher(
// optimistic update messages
await this._preUpdateMessages(userInput, attachments);
const sessionId = await this.createSessionId();
const sessionId = (await this.createSession())?.id;
let contexts = await this._getMatchedContexts();
if (abortController.signal.aborted) {
return;
@@ -678,11 +674,13 @@ export class AIChatInput extends SignalWatcher(
};
private readonly _postUpdateMessages = async () => {
const sessionId = this.session?.id;
if (!sessionId || !AIProvider.histories) return;
const { messages } = this.chatContextValue;
const last = messages[messages.length - 1] as ChatMessage;
if (!last.id) {
const sessionId = await this.getSessionId();
const historyIds = await AIProvider.histories?.ids(
const historyIds = await AIProvider.histories.ids(
this.host.store.workspace.id,
this.host.store.id,
{ sessionId }

View File

@@ -50,7 +50,7 @@ export class ChatInputPreference extends SignalWatcher(
`;
@property({ attribute: false })
accessor session!: CopilotSessionType | undefined;
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor onModelChange: ((modelId: string) => void) | undefined;

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import {
DocModeProvider,
@@ -13,25 +14,21 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { debounce } from 'lodash-es';
import { AffineIcon } from '../_common/icons';
import { AffineIcon } from '../../_common/icons';
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
import { AIPreloadConfig } from '../../chat-panel/preload-config';
import { type AIError, AIProvider, UnauthorizedError } from '../../provider';
import { mergeStreamObjects } from '../../utils/stream-objects';
import { type ChatContextValue } from '../ai-chat-content/type';
import type {
AINetworkSearchConfig,
AIReasoningConfig,
} from '../components/ai-chat-input';
import {
isChatAction,
isChatMessage,
StreamObjectSchema,
} from '../components/ai-chat-messages';
import { type AIError, AIProvider, UnauthorizedError } from '../provider';
import { mergeStreamObjects } from '../utils/stream-objects';
import { type ChatContextValue } from './chat-context';
import { HISTORY_IMAGE_ACTIONS } from './const';
import { AIPreloadConfig } from './preload-config';
} from '../ai-chat-input';
import { isChatAction, isChatMessage, StreamObjectSchema } from './type';
export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
export class AIChatMessages extends WithDisposable(ShadowlessElement) {
static override styles = css`
chat-panel-messages {
ai-chat-messages {
position: relative;
}
@@ -147,16 +144,16 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
accessor host!: EditorHost;
@property({ attribute: false })
accessor isLoading!: boolean;
accessor isHistoryLoading!: boolean;
@property({ attribute: false })
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor createSessionId!: () => Promise<string | undefined>;
accessor createSession!: () => Promise<CopilotSessionType | undefined>;
@property({ attribute: false })
accessor updateContext!: (context: Partial<ChatContextValue>) => void;
@@ -174,7 +171,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false })
accessor panelWidth!: Signal<number | undefined>;
accessor width: Signal<number | undefined> | undefined;
@query('.chat-panel-messages-container')
accessor messagesContainer: HTMLDivElement | null = null;
@@ -202,7 +199,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
}
private _renderAIOnboarding() {
return this.isLoading ||
return this.isHistoryLoading ||
!this.host?.store.get(FeatureFlagService).getFlag('enable_ai_onboarding')
? nothing
: html`<div class="onboarding-wrapper" data-testid="ai-onboarding">
@@ -241,7 +238,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
protected override render() {
const { messages, status, error } = this.chatContextValue;
const { isLoading } = this;
const { isHistoryLoading } = this;
const filteredItems = messages.filter(item => {
return (
isChatMessage(item) ||
@@ -268,12 +265,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
data-testid="chat-panel-messages-placeholder"
>
${AffineIcon(
isLoading
isHistoryLoading
? 'var(--affine-icon-secondary)'
: 'var(--affine-primary-color)'
)}
<div class="messages-placeholder-title" data-loading=${isLoading}>
${this.isLoading
<div
class="messages-placeholder-title"
data-loading=${isHistoryLoading}
>
${this.isHistoryLoading
? html`<span data-testid="chat-panel-loading-state"
>AFFiNE AI is loading history...</span
>`
@@ -295,15 +295,15 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
} else if (isChatMessage(item) && item.role === 'assistant') {
return html`<chat-message-assistant
.host=${this.host}
.session=${this.session}
.item=${item}
.isLast=${isLast}
.status=${isLast ? status : 'idle'}
.error=${isLast ? error : null}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.getSessionId=${this.getSessionId}
.retry=${() => this.retry()}
.panelWidth=${this.panelWidth}
.width=${this.width}
></chat-message-assistant>`;
} else if (isChatAction(item)) {
return html`<chat-message-action
@@ -365,7 +365,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
}
protected override updated(_changedProperties: PropertyValues) {
if (_changedProperties.has('isLoading')) {
if (_changedProperties.has('isHistoryLoading')) {
this.canScrollDown = false;
}
}
@@ -382,7 +382,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
retry = async () => {
try {
const sessionId = await this.createSessionId();
const sessionId = (await this.createSession())?.id;
if (!sessionId) return;
if (!AIProvider.actions.chat) return;
@@ -448,9 +448,3 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
}
};
}
declare global {
interface HTMLElementTagNameMap {
'chat-panel-messages': ChatPanelMessages;
}
}

View File

@@ -1 +1,2 @@
export * from './ai-chat-messages';
export * from './type';

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { NotificationProvider } from '@blocksuite/affine/shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
@@ -7,15 +8,15 @@ import type { Store } from '@blocksuite/affine/store';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import type { ChatContextValue } from '../../chat-panel/chat-context';
import { AIProvider } from '../../provider';
import type { ChatContextValue } from '../ai-chat-content';
export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor chatContextValue!: ChatContextValue;
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor host!: EditorHost;
@@ -41,15 +42,16 @@ export class AIHistoryClear extends WithDisposable(ShadowlessElement) {
return (
this.chatContextValue.status === 'loading' ||
this.chatContextValue.status === 'transmitting' ||
!this.chatContextValue.messages.length
!this.chatContextValue.messages.length ||
!this.session
);
}
private readonly _cleanupHistories = async () => {
if (this._isHistoryClearDisabled) {
if (this._isHistoryClearDisabled || !this.session) {
return;
}
const sessionId = await this.getSessionId();
const sessionId = this.session.id;
const notification = this.host.std.getOptional(NotificationProvider);
if (!notification) return;
try {

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import type { ImageSelection } from '@blocksuite/affine/shared/selection';
import { NotificationProvider } from '@blocksuite/affine/shared/services';
import type {
@@ -81,7 +82,7 @@ export class ChatActionList extends LitElement {
accessor content: string = '';
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor messageId: string | undefined = undefined;
@@ -138,7 +139,7 @@ export class ChatActionList extends LitElement {
blocks: this._currentBlockSelections,
images: this._currentImageSelections,
};
const sessionId = await this.getSessionId();
const sessionId = this.session?.id;
const success = await action.handler(
host,
content,

View File

@@ -1,3 +1,4 @@
import type { CopilotSessionType } from '@affine/graphql';
import { Tooltip } from '@blocksuite/affine/components/toolbar';
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { noop } from '@blocksuite/affine/global/utils';
@@ -110,10 +111,10 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
accessor actions: ChatAction[] = [];
@property({ attribute: false })
accessor content!: string;
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor getSessionId!: () => Promise<string | undefined>;
accessor content!: string;
@property({ attribute: false })
accessor messageId: string | undefined = undefined;
@@ -221,7 +222,7 @@ export class ChatCopyMore extends WithDisposable(LitElement) {
};
return html`<div
@click=${async () => {
const sessionId = await this.getSessionId();
const sessionId = this.session?.id;
const success = await action.handler(
host,
content,

View File

@@ -6,23 +6,22 @@ import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType, Store } from '@blocksuite/affine/store';
import { DeleteIcon, NewPageIcon } from '@blocksuite/icons/lit';
import { type Signal, signal } from '@preact/signals-core';
import { css, html, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
import { throttle } from 'lodash-es';
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
import type { ChatContextValue } from '../../chat-panel/chat-context';
import type { ChatPanelMessages } from '../../chat-panel/chat-panel-messages';
import { AIProvider } from '../../provider';
import type { DocDisplayConfig, SearchMenuConfig } from '../ai-chat-chips';
import type { ChatContextValue } from '../ai-chat-content';
import type {
AINetworkSearchConfig,
AIPlaygroundConfig,
AIReasoningConfig,
} from '../ai-chat-input';
import {
type AIChatMessages,
type ChatAction,
type ChatMessage,
type HistoryMessage,
@@ -74,7 +73,7 @@ export class PlaygroundChat extends SignalWatcher(
}
}
chat-panel-messages {
ai-chat-messages {
flex: 1;
overflow-y: hidden;
}
@@ -129,6 +128,9 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false })
accessor doc!: Store;
@property({ attribute: false })
accessor session!: CopilotSessionType | null | undefined;
@property({ attribute: false })
accessor networkSearchConfig!: AINetworkSearchConfig;
@@ -153,9 +155,6 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor session: CopilotSessionType | undefined = undefined;
@property({ attribute: false })
accessor addChat!: () => Promise<void>;
@@ -168,10 +167,8 @@ export class PlaygroundChat extends SignalWatcher(
@state()
accessor embeddingProgress: [number, number] = [0, 0];
private readonly _isVisible: Signal<boolean | undefined> = signal(true);
private readonly _chatMessagesRef: Ref<ChatPanelMessages> =
createRef<ChatPanelMessages>();
private readonly _chatMessagesRef: Ref<AIChatMessages> =
createRef<AIChatMessages>();
// request counter to track the latest request
private _updateHistoryCounter = 0;
@@ -185,22 +182,29 @@ export class PlaygroundChat extends SignalWatcher(
this.isLoading = false;
};
private readonly _getSessionId = async () => {
return this.session?.id;
};
private readonly _createSessionId = async () => {
return this.session?.id;
private readonly _createSession = async () => {
return this.session;
};
private readonly _updateHistory = async () => {
const { doc } = this;
if (!AIProvider.histories) {
return;
}
const currentRequest = ++this._updateHistoryCounter;
const sessionId = this.session?.id;
const [histories, actions] = await Promise.all([
AIProvider.histories?.chats(doc.workspace.id, doc.id),
AIProvider.histories?.actions(doc.workspace.id, doc.id),
sessionId
? AIProvider.histories.chats(
this.doc.workspace.id,
sessionId,
this.doc.id
)
: Promise.resolve([]),
this.doc.id
? AIProvider.histories.actions(this.doc.workspace.id, this.doc.id)
: Promise.resolve([]),
]);
// Check if this is still the latest request
@@ -211,12 +215,8 @@ export class PlaygroundChat extends SignalWatcher(
const chatActions = (actions || []) as ChatAction[];
const messages: HistoryMessage[] = chatActions;
const sessionId = await this._getSessionId();
const history = histories?.find(history => history.sessionId === sessionId);
if (history) {
const chatMessages = (history.messages || []) as ChatMessage[];
messages.push(...chatMessages);
}
const chatMessages = (histories?.[0]?.messages || []) as ChatMessage[];
messages.push(...chatMessages);
this.chatContextValue = {
...this.chatContextValue,
@@ -289,35 +289,33 @@ export class PlaygroundChat extends SignalWatcher(
<ai-history-clear
.host=${this.host}
.doc=${this.doc}
.getSessionId=${this._getSessionId}
.session=${this.session}
.onHistoryCleared=${this._updateHistory}
.chatContextValue=${this.chatContextValue}
></ai-history-clear>
<div class="chat-panel-delete">${DeleteIcon()}</div>
</div>
<chat-panel-messages
<ai-chat-messages
${ref(this._chatMessagesRef)}
.chatContextValue=${this.chatContextValue}
.getSessionId=${this._getSessionId}
.createSessionId=${this._createSessionId}
.updateContext=${this.updateContext}
.host=${this.host}
.isLoading=${this.isLoading}
.isHistoryLoading=${this.isLoading}
.chatContextValue=${this.chatContextValue}
.session=${this.session}
.createSession=${this._createSession}
.updateContext=${this.updateContext}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
></chat-panel-messages>
></ai-chat-messages>
<ai-chat-composer
.host=${this.host}
.doc=${this.doc}
.workspaceId=${this.doc.workspace.id}
.session=${this.session}
.getSessionId=${this._getSessionId}
.createSessionId=${this._createSessionId}
.createSession=${this._createSession}
.chatContextValue=${this.chatContextValue}
.updateContext=${this.updateContext}
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
.isVisible=${this._isVisible}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig}

View File

@@ -327,6 +327,7 @@ export class PlaygroundContent extends SignalWatcher(
<playground-chat
.host=${this.host}
.doc=${this.doc}
.session=${session}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig}
@@ -335,7 +336,6 @@ export class PlaygroundContent extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.session=${session}
.addChat=${this.addChat}
></playground-chat>
</div>

View File

@@ -2,8 +2,8 @@ import { AIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-block';
import { EdgelessAIChatBlockComponent } from './blocks/ai-chat-block/ai-chat-edgeless-block';
import { LitTranscriptionBlock } from './blocks/ai-chat-block/ai-transcription-block';
import {
AIChatMessage,
AIChatMessages,
AIChatBlockMessage,
AIChatBlockMessages,
} from './blocks/ai-chat-block/components/ai-chat-messages';
import {
ChatImage,
@@ -20,7 +20,6 @@ import { ActionMindmap } from './chat-panel/actions/mindmap';
import { ActionSlides } from './chat-panel/actions/slides';
import { ActionText } from './chat-panel/actions/text';
import { AILoading } from './chat-panel/ai-loading';
import { ChatPanelMessages } from './chat-panel/chat-panel-messages';
import { ChatMessageAction } from './chat-panel/message/action';
import { ChatMessageAssistant } from './chat-panel/message/assistant';
import { ChatMessageUser } from './chat-panel/message/user';
@@ -33,9 +32,11 @@ import { ChatPanelDocChip } from './components/ai-chat-chips/doc-chip';
import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
import { ChatPanelTagChip } from './components/ai-chat-chips/tag-chip';
import { AIChatComposer } from './components/ai-chat-composer';
import { AIChatContent } from './components/ai-chat-content';
import { AIChatInput } from './components/ai-chat-input';
import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embedding-status-tooltip';
import { ChatInputPreference } from './components/ai-chat-input/preference-popup';
import { AIChatMessages } from './components/ai-chat-messages/ai-chat-messages';
import { AIHistoryClear } from './components/ai-history-clear';
import { effects as componentAiItemEffects } from './components/ai-item';
import { AssistantAvatar } from './components/ai-message-content/assistant-avatar';
@@ -104,7 +105,8 @@ export function registerAIEffects() {
customElements.define('action-slides', ActionSlides);
customElements.define('action-text', ActionText);
customElements.define('ai-loading', AILoading);
customElements.define('chat-panel-messages', ChatPanelMessages);
customElements.define('ai-chat-content', AIChatContent);
customElements.define('ai-chat-messages', AIChatMessages);
customElements.define('chat-panel', ChatPanel);
customElements.define('ai-chat-input', AIChatInput);
customElements.define(
@@ -135,8 +137,8 @@ export function registerAIEffects() {
EdgelessAIChatBlockComponent
);
customElements.define('affine-ai-chat', AIChatBlockComponent);
customElements.define('ai-chat-message', AIChatMessage);
customElements.define('ai-chat-messages', AIChatMessages);
customElements.define('ai-chat-block-message', AIChatBlockMessage);
customElements.define('ai-chat-block-messages', AIChatBlockMessages);
customElements.define(
'ai-scrollable-text-renderer',
AIScrollableTextRenderer

View File

@@ -1,6 +1,6 @@
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { ContextEmbedStatus } from '@affine/graphql';
import type { ContextEmbedStatus, CopilotSessionType } from '@affine/graphql';
import {
CanvasElementType,
EdgelessCRUDIdentifier,
@@ -12,9 +12,7 @@ import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine/shared/services';
import type { Signal } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
import { signal } from '@preact/signals-core';
import { html, LitElement, nothing, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -94,10 +92,6 @@ export class AIChatBlockPeekView extends LitElement {
private _forkBlockId: string | undefined = undefined;
private _forkSessionId: string | undefined = undefined;
accessor isComposerVisible: Signal<boolean | undefined> = signal(true);
private readonly _deserializeHistoryChatMessages = (
historyMessagesString: string
) => {
@@ -117,14 +111,14 @@ export class AIChatBlockPeekView extends LitElement {
private readonly _constructBranchChatBlockMessages = async (
rootWorkspaceId: string,
rootDocId: string,
forkSessionId: string
forkSessionId: string,
docId?: string
) => {
const currentUserInfo = await AIProvider.userInfo;
const forkMessages = (await queryHistoryMessages(
rootWorkspaceId,
rootDocId,
forkSessionId
forkSessionId,
docId
)) as ChatMessage[];
const forkLength = forkMessages.length;
const historyLength = this._historyMessages.length;
@@ -163,18 +157,20 @@ export class AIChatBlockPeekView extends LitElement {
messages: [],
});
this._forkBlockId = undefined;
this._forkSessionId = undefined;
};
private readonly _getSessionId = async () => {
return this._forkSessionId ?? this._sessionId;
private readonly initSession = async () => {
const session = await AIProvider.session?.getSession(
this.rootWorkspaceId,
this._sessionId
);
this.session = session ?? null;
};
private readonly _createSessionId = async () => {
if (this._forkSessionId) {
return this._forkSessionId;
private readonly createForkSession = async () => {
if (this.forkSession) {
return this.forkSession;
}
const lastMessage = this._historyMessages.at(-1);
if (!lastMessage) return;
@@ -185,8 +181,14 @@ export class AIChatBlockPeekView extends LitElement {
sessionId: this._sessionId,
latestMessageId: lastMessage.id,
});
this._forkSessionId = forkSessionId;
return this._forkSessionId;
if (forkSessionId) {
const session = await AIProvider.session?.getSession(
this.rootWorkspaceId,
forkSessionId
);
this.forkSession = session ?? null;
}
return this.forkSession;
};
private readonly _onChatSuccess = async () => {
@@ -213,7 +215,8 @@ export class AIChatBlockPeekView extends LitElement {
}
// If there is no session id or chat messages, do not create a new chat block
if (!this._forkSessionId || !this.chatContext.messages.length) {
const forkSessionId = this.forkSession?.id;
if (!forkSessionId || !this.chatContext.messages.length) {
return;
}
@@ -230,8 +233,8 @@ export class AIChatBlockPeekView extends LitElement {
const { rootWorkspaceId, rootDocId } = this;
const messages = await this._constructBranchChatBlockMessages(
rootWorkspaceId,
rootDocId,
this._forkSessionId
forkSessionId,
rootDocId
);
if (!messages.length) {
return;
@@ -245,7 +248,7 @@ export class AIChatBlockPeekView extends LitElement {
{
xywh: bound.serialize(),
messages: JSON.stringify(messages),
sessionId: this._forkSessionId,
sessionId: forkSessionId,
rootWorkspaceId: rootWorkspaceId,
rootDocId: rootDocId,
},
@@ -280,7 +283,8 @@ export class AIChatBlockPeekView extends LitElement {
* Update the current chat messages with the new message
*/
updateChatBlockMessages = async () => {
if (!this._forkBlockId || !this._forkSessionId) {
const forkSessionId = this.forkSession?.id;
if (!this._forkBlockId || !forkSessionId) {
return;
}
@@ -292,8 +296,8 @@ export class AIChatBlockPeekView extends LitElement {
const { rootWorkspaceId, rootDocId } = this;
const messages = await this._constructBranchChatBlockMessages(
rootWorkspaceId,
rootDocId,
this._forkSessionId
forkSessionId,
rootDocId
);
if (!messages.length) {
return;
@@ -355,8 +359,8 @@ export class AIChatBlockPeekView extends LitElement {
*/
retry = async () => {
try {
const { _forkBlockId, _forkSessionId } = this;
if (!_forkBlockId || !_forkSessionId) return;
const forkSessionId = this.forkSession?.id;
if (!this._forkBlockId || !forkSessionId) return;
if (!AIProvider.actions.chat) return;
const abortController = new AbortController();
@@ -376,7 +380,7 @@ export class AIChatBlockPeekView extends LitElement {
const { store } = this.host;
const stream = await AIProvider.actions.chat({
sessionId: _forkSessionId,
sessionId: forkSessionId,
retry: true,
docId: store.id,
workspaceId: store.workspace.id,
@@ -461,20 +465,20 @@ export class AIChatBlockPeekView extends LitElement {
}
return html`<div class=${messageClasses}>
<ai-chat-message
<ai-chat-block-message
.host=${host}
.state=${messageState}
.message=${message}
.textRendererOptions=${this._textRendererOptions}
></ai-chat-message>
></ai-chat-block-message>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
${shouldRenderCopyMore
? html` <chat-copy-more
.host=${host}
.session=${this.forkSession}
.actions=${actions}
.content=${markdown}
.isLast=${isLastReply}
.getSessionId=${this._getSessionId}
.messageId=${message.id ?? undefined}
.retry=${() => this.retry()}
></chat-copy-more>`
@@ -482,9 +486,9 @@ export class AIChatBlockPeekView extends LitElement {
${shouldRenderActions
? html`<chat-action-list
.host=${host}
.session=${this.forkSession}
.actions=${actions}
.content=${markdown}
.getSessionId=${this._getSessionId}
.messageId=${message.id ?? undefined}
.layoutDirection=${'horizontal'}
></chat-action-list>`
@@ -496,6 +500,7 @@ export class AIChatBlockPeekView extends LitElement {
override connectedCallback() {
super.connectedCallback();
this.initSession().catch(console.error);
const extensions = this.host.std
.get(ViewExtensionManagerIdentifier)
.get('preview-page');
@@ -507,8 +512,8 @@ export class AIChatBlockPeekView extends LitElement {
this._historyMessages = this._deserializeHistoryChatMessages(
this.historyMessagesString
);
const { rootWorkspaceId, rootDocId, _sessionId } = this;
queryHistoryMessages(rootWorkspaceId, rootDocId, _sessionId)
const { rootWorkspaceId, _sessionId } = this;
queryHistoryMessages(rootWorkspaceId, _sessionId)
.then(messages => {
this._historyMessages = this._historyMessages.map((message, idx) => {
return {
@@ -566,17 +571,17 @@ export class AIChatBlockPeekView extends LitElement {
<ai-history-clear
.host=${this.host}
.doc=${this.host.store}
.getSessionId=${this._getSessionId}
.session=${this.forkSession}
.onHistoryCleared=${this._onHistoryCleared}
.chatContextValue=${chatContext}
></ai-history-clear>
</div>
<div class="ai-chat-messages-container">
<ai-chat-messages
<ai-chat-block-messages
.host=${host}
.messages=${_historyMessages}
.textRendererOptions=${_textRendererOptions}
></ai-chat-messages>
></ai-chat-block-messages>
<date-time .date=${latestMessageCreatedAt}></date-time>
<div class="new-chat-messages-container">
${this.CurrentMessages(currentChatMessages)}
@@ -584,12 +589,11 @@ export class AIChatBlockPeekView extends LitElement {
</div>
<ai-chat-composer
.host=${host}
.doc=${this.host.store}
.getSessionId=${this._getSessionId}
.createSessionId=${this._createSessionId}
.workspaceId=${this.rootWorkspaceId}
.session=${this.forkSession ?? this.session}
.createSession=${this.createForkSession}
.chatContextValue=${chatContext}
.updateContext=${updateContext}
.isVisible=${this.isComposerVisible}
.updateEmbeddingProgress=${this._updateEmbeddingProgress}
.networkSearchConfig=${networkSearchConfig}
.docDisplayConfig=${this.docDisplayConfig}
@@ -647,6 +651,12 @@ export class AIChatBlockPeekView extends LitElement {
@state()
accessor embeddingProgress: [number, number] = [0, 0];
@state()
accessor session: CopilotSessionType | null | undefined;
@state()
accessor forkSession: CopilotSessionType | null | undefined;
}
declare global {

View File

@@ -1,8 +1,8 @@
import type { EditorHost } from '@blocksuite/affine/std';
import { captureException } from '@sentry/react';
import { Subject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
import type { ChatContextValue } from '../chat-panel/chat-context';
import type { ChatContextValue } from '../components/ai-chat-content';
import {
PaymentRequiredError,
RequestTimeoutError,
@@ -133,8 +133,8 @@ export class AIProvider {
// use case: when user selects "continue in chat" in an ask ai result panel
// do we need to pass the context to the chat panel?
/* eslint-disable rxjs/finnish */
requestOpenWithChat: new Subject<AIChatParams>(),
requestSendWithChat: new Subject<AISendParams>(),
requestOpenWithChat: new BehaviorSubject<AIChatParams | null>(null),
requestSendWithChat: new BehaviorSubject<AISendParams | null>(null),
requestInsertTemplate: new Subject<{
template: string;
mode: 'page' | 'edgeless';

View File

@@ -2,7 +2,6 @@ import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onb
import type { AuthAccountInfo, AuthService } from '@affine/core/modules/cloud';
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
import {
type ChatHistoryOrder,
ContextCategories,
type ContextWorkspaceEmbeddingStatus,
type getCopilotHistoriesQuery,
@@ -742,7 +741,7 @@ Could you make a new website based on these notes and send back just the html fi
AIProvider.provide('histories', {
actions: async (
workspaceId: string,
docId?: string
docId: string
): Promise<BlockSuitePresets.AIHistory[]> => {
// @ts-expect-error - 'action' is missing in server impl
return (
@@ -754,14 +753,15 @@ Could you make a new website based on these notes and send back just the html fi
},
chats: async (
workspaceId: string,
docId?: string,
options?: {
sessionId?: string;
messageOrder?: ChatHistoryOrder;
}
sessionId: string,
docId?: string
): Promise<BlockSuitePresets.AIHistory[]> => {
// @ts-expect-error - 'action' is missing in server impl
return (await client.getHistories(workspaceId, docId, options)) ?? [];
return (
(await client.getHistories(workspaceId, docId, {
sessionId,
})) ?? []
);
},
cleanup: async (
workspaceId: string,

View File

@@ -23,7 +23,7 @@ import type { EditorHost } from '@blocksuite/affine/std';
import type { BlockModel, Store } from '@blocksuite/affine/store';
import { Slice, toDraftModel } from '@blocksuite/affine/store';
import type { ChatContextValue } from '../chat-panel/chat-context';
import type { ChatContextValue } from '../components/ai-chat-content';
import {
getSelectedImagesAsBlobs,
getSelectedTextContent,

View File

@@ -1,6 +1,6 @@
import { Scrollable } from '@affine/component';
import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
import type { ChatPanel } from '@affine/core/blocksuite/ai';
import type { AIChatParams, ChatPanel } from '@affine/core/blocksuite/ai';
import { AIProvider } from '@affine/core/blocksuite/ai';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
@@ -119,7 +119,10 @@ const DetailPageImpl = memo(function DetailPageImpl() {
useEffect(() => {
const disposables: Subscription[] = [];
const openHandler = () => {
const openHandler = (params: AIChatParams | null) => {
if (!params) {
return;
}
workbench.openSidebar();
view.activeSidebarTab('chat');
};

View File

@@ -1,6 +1,6 @@
import { Scrollable } from '@affine/component';
import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
import { AIProvider } from '@affine/core/blocksuite/ai';
import { type AIChatParams, AIProvider } from '@affine/core/blocksuite/ai';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
@@ -127,7 +127,10 @@ function DocPeekPreviewEditor({
useEffect(() => {
const disposables: Subscription[] = [];
const openHandler = () => {
const openHandler = (params: AIChatParams | null) => {
if (!params) {
return;
}
if (doc) {
workbench.openDoc(doc.id);
peekView.close();