diff --git a/packages/backend/server/src/models/copilot-session.ts b/packages/backend/server/src/models/copilot-session.ts
index aecc1a898..5d64e3667 100644
--- a/packages/backend/server/src/models/copilot-session.ts
+++ b/packages/backend/server/src/models/copilot-session.ts
@@ -194,6 +194,7 @@ export class CopilotSessionModel extends BaseModel {
id: true,
role: true,
content: true,
+ streamObjects: true,
attachments: true,
params: true,
createdAt: true,
diff --git a/packages/frontend/core/src/blocksuite/ai/_common/chat-actions-handle.ts b/packages/frontend/core/src/blocksuite/ai/_common/chat-actions-handle.ts
index 06dba04e3..54189fc47 100644
--- a/packages/frontend/core/src/blocksuite/ai/_common/chat-actions-handle.ts
+++ b/packages/frontend/core/src/blocksuite/ai/_common/chat-actions-handle.ts
@@ -91,7 +91,7 @@ export function constructUserInfoWithMessages(
userInfo: AIUserInfo | null
) {
return messages.map(message => {
- const { role, id, content, createdAt } = message;
+ const { role, streamObjects } = message;
const isUser = role === 'user';
const userInfoProps = isUser
? {
@@ -101,12 +101,10 @@ export function constructUserInfoWithMessages(
}
: {};
return {
- id,
- role,
- content,
- createdAt,
- attachments: [],
+ ...message,
...userInfoProps,
+ attachments: [],
+ streamObjects: streamObjects || [],
};
});
}
@@ -117,11 +115,11 @@ export async function constructRootChatBlockMessages(
) {
// Convert chat messages to AI chat block messages
const userInfo = await AIProvider.userInfo;
- const forkMessages = await queryHistoryMessages(
+ const forkMessages = (await queryHistoryMessages(
doc.workspace.id,
doc.id,
forkSessionId
- );
+ )) as ChatMessage[];
return constructUserInfoWithMessages(forkMessages, userInfo);
}
diff --git a/packages/frontend/core/src/blocksuite/ai/actions/types.ts b/packages/frontend/core/src/blocksuite/ai/actions/types.ts
index cebe62617..d0e31668a 100644
--- a/packages/frontend/core/src/blocksuite/ai/actions/types.ts
+++ b/packages/frontend/core/src/blocksuite/ai/actions/types.ts
@@ -9,6 +9,7 @@ import type {
CopilotSessionType,
getCopilotHistoriesQuery,
RequestOptions,
+ StreamObject,
} from '@affine/graphql';
import type { EditorHost } from '@blocksuite/affine/std';
import type { GfxModel } from '@blocksuite/affine/std/gfx';
@@ -359,7 +360,8 @@ declare global {
content: string;
createdAt: string;
role: MessageRole;
- attachments?: string[];
+ attachments?: string[] | null;
+ streamObjects?: StreamObject[] | null;
}[];
}
diff --git a/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts b/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts
index 5f84a6603..7f697cbf4 100644
--- a/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts
+++ b/packages/frontend/core/src/blocksuite/ai/blocks/ai-chat-block/components/ai-chat-messages.ts
@@ -1,14 +1,13 @@
import type { TextRendererOptions } from '@affine/core/blocksuite/ai/components/text-renderer';
-import type { AffineAIPanelState } from '@affine/core/blocksuite/ai/widgets/ai-panel/type';
import type { EditorHost } from '@blocksuite/affine/std';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
-import type {
- ChatMessage,
- MessageRole,
+import {
+ type ChatMessage,
+ type StreamObject,
} from '../../../components/ai-chat-messages';
import { UserInfoTemplate } from './user-info';
@@ -36,16 +35,14 @@ export class AIChatMessage extends LitElement {
override render() {
const {
- host,
- textRendererOptions,
- state,
content,
attachments,
- messageRole,
- userId,
userName,
+ userId,
avatarUrl,
- } = this;
+ role,
+ streamObjects,
+ } = this.message;
const withAttachments = !!attachments && attachments.length > 0;
const messageClasses = classMap({
@@ -54,48 +51,52 @@ export class AIChatMessage extends LitElement {
return html`
- ${UserInfoTemplate({ userId, userName, avatarUrl }, messageRole)}
+ ${UserInfoTemplate({ userId, userName, avatarUrl }, role)}
-
+ ${streamObjects?.length
+ ? this.renderStreamObjects(streamObjects)
+ : this.renderRichText(content)}
`;
}
- @property({ attribute: false })
- accessor attachments: string[] | undefined = undefined;
+ private renderStreamObjects(answer: StreamObject[]) {
+ return html``;
+ }
+
+ private renderRichText(text: string) {
+ return html``;
+ }
@property({ attribute: false })
- accessor content: string = '';
+ accessor message!: ChatMessage;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
- accessor messageRole: MessageRole | undefined = undefined;
-
- @property({ attribute: false })
- accessor state: AffineAIPanelState = 'finished';
+ accessor state: 'finished' | 'generating' = 'finished';
@property({ attribute: false })
accessor textRendererOptions: TextRendererOptions = {};
-
- @property({ attribute: false })
- accessor userId: string | undefined = undefined;
-
- @property({ attribute: false })
- accessor userName: string | undefined = undefined;
-
- @property({ attribute: false })
- accessor avatarUrl: string | undefined = undefined;
}
export class AIChatMessages extends LitElement {
@@ -121,18 +122,11 @@ export class AIChatMessages extends LitElement {
this.messages,
message => message.id || message.createdAt,
message => {
- const { attachments, role, content, userId, userName, avatarUrl } =
- message;
return html`
`;
}
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts
index 89be5d5a7..f089bf783 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/actions/image.ts
@@ -1,6 +1,3 @@
-import './action-wrapper';
-import '../content/images';
-
import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts
index c9bba28a1..1c501f1b5 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts
@@ -19,7 +19,6 @@ import type {
AIReasoningConfig,
} from '../components/ai-chat-input';
import {
- type ChatMessage,
isChatAction,
isChatMessage,
StreamObjectSchema,
@@ -418,22 +417,27 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
});
for await (const text of stream) {
- const messages = [...this.chatContextValue.messages];
- const last = messages[messages.length - 1] as ChatMessage;
- try {
- const parsed = StreamObjectSchema.safeParse(JSON.parse(text));
- if (parsed.success) {
- last.streamObjects = mergeStreamObjects([
+ const messages = this.chatContextValue.messages.slice(0);
+ const last = messages.at(-1);
+ if (last && isChatMessage(last)) {
+ try {
+ const parsed = StreamObjectSchema.parse(JSON.parse(text));
+ const streamObjects = mergeStreamObjects([
...(last.streamObjects ?? []),
- parsed.data,
+ parsed,
]);
- } else {
- last.content += text;
+ messages[messages.length - 1] = {
+ ...last,
+ streamObjects,
+ };
+ } catch {
+ messages[messages.length - 1] = {
+ ...last,
+ content: last.content + text,
+ };
}
- } catch {
- last.content += text;
+ this.updateContext({ messages, status: 'transmitting' });
}
- this.updateContext({ messages, status: 'transmitting' });
}
this.updateContext({ status: 'success' });
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
index aed072bce..1e32663af 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
@@ -24,7 +24,11 @@ import type {
AINetworkSearchConfig,
AIReasoningConfig,
} from '../components/ai-chat-input';
-import { type HistoryMessage } from '../components/ai-chat-messages';
+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';
@@ -146,12 +150,14 @@ export class ChatPanel extends SignalWatcher(
return;
}
- const messages: HistoryMessage[] = actions ? [...actions] : [];
+ const chatActions = (actions || []) as ChatAction[];
+ const messages: HistoryMessage[] = chatActions;
const sessionId = await this._getSessionId();
const history = histories?.find(history => history.sessionId === sessionId);
if (history) {
- messages.push(...history.messages);
+ const chatMessages = (history.messages || []) as ChatMessage[];
+ messages.push(...chatMessages);
}
this.chatContextValue = {
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts
index 491bf7ef5..2c1b5eb5b 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/action.ts
@@ -1,5 +1,3 @@
-import '../content/assistant-avatar';
-
import { WithDisposable } from '@blocksuite/affine/global/lit';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts
index 4409c40cb..8400d0be8 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/assistant.ts
@@ -1,9 +1,5 @@
-import '../content/assistant-avatar';
-import '../content/rich-text';
-
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WithDisposable } from '@blocksuite/affine/global/lit';
-import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
@@ -33,20 +29,6 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
font-size: var(--affine-font-xs);
font-weight: 400;
}
-
- .reasoning-wrapper {
- padding: 16px 20px;
- margin: 8px 0;
- border-radius: 8px;
- background-color: rgba(0, 0, 0, 0.05);
- }
-
- .tool-wrapper {
- padding: 12px;
- margin: 8px 0;
- border-radius: 8px;
- border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
- }
`;
@property({ attribute: false })
@@ -82,6 +64,15 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor panelWidth!: Signal;
+ get state() {
+ const { isLast, status } = this;
+ return isLast
+ ? status !== 'loading' && status !== 'transmitting'
+ ? 'finished'
+ : 'generating'
+ : 'finished';
+ }
+
renderHeader() {
const isWithDocs =
'content' in this.item &&
@@ -122,102 +113,21 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
}
private renderStreamObjects(answer: StreamObject[]) {
- return html`
- ${answer.map(data => {
- switch (data.type) {
- case 'text-delta':
- return this.renderRichText(data.textDelta);
- case 'reasoning':
- return html`
-
- ${this.renderRichText(data.textDelta)}
-
- `;
- case 'tool-call':
- return this.renderToolCall(data);
- case 'tool-result':
- return this.renderToolResult(data);
- default:
- return nothing;
- }
- })}
-
`;
- }
-
- private renderToolCall(streamObject: StreamObject) {
- if (streamObject.type !== 'tool-call') {
- return nothing;
- }
-
- switch (streamObject.toolName) {
- case 'web_crawl_exa':
- return html`
-
- `;
- case 'web_search_exa':
- return html`
-
- `;
- default:
- return html`
-
- ${streamObject.toolName} tool calling...
-
- `;
- }
- }
-
- private renderToolResult(streamObject: StreamObject) {
- if (streamObject.type !== 'tool-result') {
- return nothing;
- }
-
- switch (streamObject.toolName) {
- case 'web_crawl_exa':
- return html`
-
- `;
- case 'web_search_exa':
- return html`
-
- `;
- default:
- return html`
-
- ${streamObject.toolName} tool result...
-
- `;
- }
+ return html``;
}
private renderRichText(text: string) {
- const { host, isLast, status } = this;
- const state = isLast
- ? status !== 'loading' && status !== 'transmitting'
- ? 'finished'
- : 'generating'
- : 'finished';
-
return html``;
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/user.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/user.ts
index 0deea19f8..73c78674b 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/message/user.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/message/user.ts
@@ -1,6 +1,3 @@
-import '../content/images';
-import '../content/pure-text';
-
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { ShadowlessElement } from '@blocksuite/affine/std';
import { css, html, nothing } from 'lit';
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts
index bae888ae7..ae5dc5211 100644
--- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts
@@ -19,7 +19,11 @@ import { readBlobAsURL } from '../../utils/image';
import { mergeStreamObjects } from '../../utils/stream-objects';
import type { ChatChip, DocDisplayConfig } from '../ai-chat-chips/type';
import { isDocChip } from '../ai-chat-chips/utils';
-import { type ChatMessage, StreamObjectSchema } from '../ai-chat-messages';
+import {
+ type ChatMessage,
+ isChatMessage,
+ StreamObjectSchema,
+} from '../ai-chat-messages';
import { MAX_IMAGE_COUNT } from './const';
import type {
AIChatInputContext,
@@ -609,22 +613,27 @@ export class AIChatInput extends SignalWatcher(
});
for await (const text of stream) {
- const messages = [...this.chatContextValue.messages];
- const last = messages[messages.length - 1] as ChatMessage;
- try {
- const parsed = StreamObjectSchema.safeParse(JSON.parse(text));
- if (parsed.success) {
- last.streamObjects = mergeStreamObjects([
+ const messages = this.chatContextValue.messages.slice(0);
+ const last = messages.at(-1);
+ if (last && isChatMessage(last)) {
+ try {
+ const parsed = StreamObjectSchema.parse(JSON.parse(text));
+ const streamObjects = mergeStreamObjects([
...(last.streamObjects ?? []),
- parsed.data,
+ parsed,
]);
- } else {
- last.content += text;
+ messages[messages.length - 1] = {
+ ...last,
+ streamObjects,
+ };
+ } catch {
+ messages[messages.length - 1] = {
+ ...last,
+ content: last.content + text,
+ };
}
- } catch {
- last.content += text;
+ this.updateContext({ messages, status: 'transmitting' });
}
- this.updateContext({ messages, status: 'transmitting' });
}
this.updateContext({ status: 'success' });
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/assistant-avatar.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/assistant-avatar.ts
similarity index 93%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/content/assistant-avatar.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-message-content/assistant-avatar.ts
index 4f62e7e97..dd12f9ba2 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/assistant-avatar.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/assistant-avatar.ts
@@ -4,7 +4,7 @@ import { AiIcon } from '@blocksuite/icons/lit';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
-import type { ChatStatus } from '../../components/ai-chat-messages';
+import type { ChatStatus } from '../ai-chat-messages';
const AffineAvatarIcon = AiIcon({
width: '20px',
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/images.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/images.ts
similarity index 100%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/content/images.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-message-content/images.ts
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/pure-text.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/pure-text.ts
similarity index 100%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/content/pure-text.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-message-content/pure-text.ts
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts
similarity index 100%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts
new file mode 100644
index 000000000..d72576c88
--- /dev/null
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts
@@ -0,0 +1,145 @@
+import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
+import { WithDisposable } from '@blocksuite/affine/global/lit';
+import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
+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, nothing } from 'lit';
+import { property } from 'lit/decorators.js';
+
+import type { AffineAIPanelState } from '../../widgets/ai-panel/type';
+import type { StreamObject } from '../ai-chat-messages';
+
+export class ChatContentStreamObjects extends WithDisposable(
+ ShadowlessElement
+) {
+ static override styles = css`
+ .reasoning-wrapper {
+ padding: 16px 20px;
+ margin: 8px 0;
+ border-radius: 8px;
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+
+ .tool-wrapper {
+ padding: 12px;
+ margin: 8px 0;
+ border-radius: 8px;
+ border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
+ }
+ `;
+
+ @property({ attribute: false })
+ accessor answer!: StreamObject[];
+
+ @property({ attribute: false })
+ accessor host!: EditorHost;
+
+ @property({ attribute: false })
+ accessor state: AffineAIPanelState = 'finished';
+
+ @property({ attribute: false })
+ accessor width: Signal | undefined;
+
+ @property({ attribute: false })
+ accessor extensions!: ExtensionType[];
+
+ @property({ attribute: false })
+ accessor affineFeatureFlagService!: FeatureFlagService;
+
+ private renderToolCall(streamObject: StreamObject) {
+ if (streamObject.type !== 'tool-call') {
+ return nothing;
+ }
+
+ switch (streamObject.toolName) {
+ case 'web_crawl_exa':
+ return html`
+
+ `;
+ case 'web_search_exa':
+ return html`
+
+ `;
+ default:
+ return html`
+
+ ${streamObject.toolName} tool calling...
+
+ `;
+ }
+ }
+
+ private renderToolResult(streamObject: StreamObject) {
+ if (streamObject.type !== 'tool-result') {
+ return nothing;
+ }
+
+ switch (streamObject.toolName) {
+ case 'web_crawl_exa':
+ return html`
+
+ `;
+ case 'web_search_exa':
+ return html`
+
+ `;
+ default:
+ return html`
+
+ ${streamObject.toolName} tool result...
+
+ `;
+ }
+ }
+
+ private renderRichText(text: string) {
+ return html``;
+ }
+
+ protected override render() {
+ return html`
+ ${this.answer.map(data => {
+ switch (data.type) {
+ case 'text-delta':
+ return this.renderRichText(data.textDelta);
+ case 'reasoning':
+ return html`
+
+ ${this.renderRichText(data.textDelta)}
+
+ `;
+ case 'tool-call':
+ return this.renderToolCall(data);
+ case 'tool-result':
+ return this.renderToolResult(data);
+ default:
+ return nothing;
+ }
+ })}
+
`;
+ }
+}
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/tools/tool-call-card.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-call-card.ts
similarity index 100%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/tools/tool-call-card.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-call-card.ts
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/tools/tool-result-card.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts
similarity index 96%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/tools/tool-result-card.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts
index 6da24e66e..7ef3f49e5 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/tools/tool-result-card.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts
@@ -3,7 +3,7 @@ import { ImageProxyService } from '@blocksuite/affine/shared/adapters';
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
import { ToggleDownIcon } from '@blocksuite/icons/lit';
-import { type Signal, signal } from '@preact/signals-core';
+import { type Signal } from '@preact/signals-core';
import { css, html, nothing, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
@@ -170,7 +170,7 @@ export class ToolResultCard extends SignalWatcher(
accessor results!: ToolResult[];
@property({ attribute: false })
- accessor width: Signal = signal(undefined);
+ accessor width: Signal | undefined;
@state()
private accessor isCollapsed = true;
@@ -209,7 +209,10 @@ export class ToolResultCard extends SignalWatcher(
return nothing;
}
- const maxIcons = Number(this.width.value) <= 400 ? 1 : 3;
+ let maxIcons = 3;
+ if (this.width && this.width.value !== undefined) {
+ maxIcons = this.width.value <= 400 ? 1 : 3;
+ }
const visibleIcons = this.footerIcons.slice(0, maxIcons);
return html`
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/tools/web-crawl.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts
similarity index 97%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/tools/web-crawl.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts
index b70a6c52b..7ba9d4a55 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/tools/web-crawl.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-crawl.ts
@@ -35,7 +35,7 @@ export class WebCrawlTool extends WithDisposable(ShadowlessElement) {
accessor host!: EditorHost;
@property({ attribute: false })
- accessor width!: Signal;
+ accessor width: Signal | undefined;
renderToolCall() {
return html`
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/tools/web-search.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts
similarity index 97%
rename from packages/frontend/core/src/blocksuite/ai/chat-panel/tools/web-search.ts
rename to packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts
index 7aaa78fd2..754c05faf 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/tools/web-search.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/web-search.ts
@@ -35,7 +35,7 @@ export class WebSearchTool extends WithDisposable(ShadowlessElement) {
accessor host!: EditorHost;
@property({ attribute: false })
- accessor width!: Signal;
+ accessor width: Signal | undefined;
renderToolCall() {
return html`
diff --git a/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts b/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts
index b4753dfac..7372358d8 100644
--- a/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/playground/chat.ts
@@ -22,7 +22,11 @@ import type {
AINetworkSearchConfig,
AIReasoningConfig,
} from '../ai-chat-input';
-import { type HistoryMessage } from '../ai-chat-messages';
+import {
+ type ChatAction,
+ type ChatMessage,
+ type HistoryMessage,
+} from '../ai-chat-messages';
const DEFAULT_CHAT_CONTEXT_VALUE: ChatContextValue = {
quote: '',
@@ -204,12 +208,14 @@ export class PlaygroundChat extends SignalWatcher(
return;
}
- const messages: HistoryMessage[] = actions ? [...actions] : [];
+ const chatActions = (actions || []) as ChatAction[];
+ const messages: HistoryMessage[] = chatActions;
const sessionId = await this._getSessionId();
const history = histories?.find(history => history.sessionId === sessionId);
if (history) {
- messages.push(...history.messages);
+ const chatMessages = (history.messages || []) as ChatMessage[];
+ messages.push(...chatMessages);
}
this.chatContextValue = {
diff --git a/packages/frontend/core/src/blocksuite/ai/effects.ts b/packages/frontend/core/src/blocksuite/ai/effects.ts
index 36342c502..058004bba 100644
--- a/packages/frontend/core/src/blocksuite/ai/effects.ts
+++ b/packages/frontend/core/src/blocksuite/ai/effects.ts
@@ -21,17 +21,9 @@ 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 { AssistantAvatar } from './chat-panel/content/assistant-avatar';
-import { ChatContentImages } from './chat-panel/content/images';
-import { ChatContentPureText } from './chat-panel/content/pure-text';
-import { ChatContentRichText } from './chat-panel/content/rich-text';
import { ChatMessageAction } from './chat-panel/message/action';
import { ChatMessageAssistant } from './chat-panel/message/assistant';
import { ChatMessageUser } from './chat-panel/message/user';
-import { ToolCallCard } from './chat-panel/tools/tool-call-card';
-import { ToolResultCard } from './chat-panel/tools/tool-result-card';
-import { WebCrawlTool } from './chat-panel/tools/web-crawl';
-import { WebSearchTool } from './chat-panel/tools/web-search';
import { ChatPanelAddPopover } from './components/ai-chat-chips/add-popover';
import { ChatPanelCandidatesPopover } from './components/ai-chat-chips/candidates-popover';
import { ChatPanelChips } from './components/ai-chat-chips/chat-panel-chips';
@@ -46,7 +38,16 @@ import { AIChatEmbeddingStatusTooltip } from './components/ai-chat-input/embeddi
import { ChatInputPreference } from './components/ai-chat-input/preference-popup';
import { AIHistoryClear } from './components/ai-history-clear';
import { effects as componentAiItemEffects } from './components/ai-item';
+import { AssistantAvatar } from './components/ai-message-content/assistant-avatar';
+import { ChatContentImages } from './components/ai-message-content/images';
+import { ChatContentPureText } from './components/ai-message-content/pure-text';
+import { ChatContentRichText } from './components/ai-message-content/rich-text';
+import { ChatContentStreamObjects } from './components/ai-message-content/stream-objects';
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
+import { ToolCallCard } from './components/ai-tools/tool-call-card';
+import { ToolResultCard } from './components/ai-tools/tool-result-card';
+import { WebCrawlTool } from './components/ai-tools/web-crawl';
+import { WebSearchTool } from './components/ai-tools/web-search';
import { AskAIButton } from './components/ask-ai-button';
import { AskAIIcon } from './components/ask-ai-icon';
import { AskAIPanel } from './components/ask-ai-panel';
@@ -156,6 +157,10 @@ export function registerAIEffects() {
customElements.define('chat-content-images', ChatContentImages);
customElements.define('chat-content-pure-text', ChatContentPureText);
customElements.define('chat-content-rich-text', ChatContentRichText);
+ customElements.define(
+ 'chat-content-stream-objects',
+ ChatContentStreamObjects
+ );
customElements.define('chat-message-action', ChatMessageAction);
customElements.define('chat-message-assistant', ChatMessageAssistant);
customElements.define('chat-message-user', ChatMessageUser);
diff --git a/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts b/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts
index ecc666361..177f7773c 100644
--- a/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts
+++ b/packages/frontend/core/src/blocksuite/ai/peek-view/chat-block-peek-view.ts
@@ -14,10 +14,11 @@ import {
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 } from 'lit';
+import { html, LitElement, nothing, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
+import { throttle } from 'lodash-es';
import {
ChatBlockPeekViewActions,
@@ -36,12 +37,16 @@ import type {
import type { ChatMessage } from '../components/ai-chat-messages';
import {
ChatMessagesSchema,
+ isChatMessage,
StreamObjectSchema,
} from '../components/ai-chat-messages';
import type { TextRendererOptions } from '../components/text-renderer';
import { AIChatErrorRenderer } from '../messages/error';
import { type AIError, AIProvider } from '../provider';
-import { mergeStreamObjects } from '../utils/stream-objects';
+import {
+ mergeStreamContent,
+ mergeStreamObjects,
+} from '../utils/stream-objects';
import { PeekViewStyles } from './styles';
import type { ChatContext } from './types';
import { calcChildBound } from './utils';
@@ -115,11 +120,11 @@ export class AIChatBlockPeekView extends LitElement {
forkSessionId: string
) => {
const currentUserInfo = await AIProvider.userInfo;
- const forkMessages = await queryHistoryMessages(
+ const forkMessages = (await queryHistoryMessages(
rootWorkspaceId,
rootDocId,
forkSessionId
- );
+ )) as ChatMessage[];
const forkLength = forkMessages.length;
const historyLength = this._historyMessages.length;
@@ -332,6 +337,18 @@ export class AIChatBlockPeekView extends LitElement {
this._resetContext();
};
+ private readonly _scrollToEnd = () => {
+ requestAnimationFrame(() => {
+ if (!this._chatMessagesContainer) return;
+ this._chatMessagesContainer.scrollTo({
+ top: this._chatMessagesContainer.scrollHeight,
+ behavior: 'smooth',
+ });
+ });
+ };
+
+ private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 600);
+
/**
* Retry the last chat message
*/
@@ -372,22 +389,27 @@ export class AIChatBlockPeekView extends LitElement {
});
for await (const text of stream) {
- const messages = [...this.chatContext.messages];
- const last = messages[messages.length - 1] as ChatMessage;
- try {
- const parsed = StreamObjectSchema.safeParse(JSON.parse(text));
- if (parsed.success) {
- last.streamObjects = mergeStreamObjects([
+ const messages = this.chatContext.messages.slice(0);
+ const last = messages.at(-1);
+ if (last && isChatMessage(last)) {
+ try {
+ const parsed = StreamObjectSchema.parse(JSON.parse(text));
+ const streamObjects = mergeStreamObjects([
...(last.streamObjects ?? []),
- parsed.data,
+ parsed,
]);
- } else {
- last.content += text;
+ messages[messages.length - 1] = {
+ ...last,
+ streamObjects,
+ };
+ } catch {
+ messages[messages.length - 1] = {
+ ...last,
+ content: last.content + text,
+ };
}
- } catch {
- last.content += text;
+ this.updateContext({ messages, status: 'transmitting' });
}
- this.updateContext({ messages, status: 'transmitting' });
}
this.updateContext({ status: 'success' });
@@ -410,7 +432,7 @@ export class AIChatBlockPeekView extends LitElement {
return html`${repeat(
currentMessages,
- message => message.id || message.createdAt,
+ (_, index) => index,
(message, idx) => {
const { status, error } = this.chatContext;
const isAssistantMessage = message.role === 'assistant';
@@ -424,26 +446,24 @@ export class AIChatBlockPeekView extends LitElement {
const isNotReady = status === 'transmitting' || status === 'loading';
const shouldRenderCopyMore =
isAssistantMessage && !(isLastReply && isNotReady);
- const shouldRenderActions =
- isLastReply && !!message.content && !isNotReady;
+ const markdown = message.streamObjects?.length
+ ? mergeStreamContent(message.streamObjects)
+ : message.content;
+ const shouldRenderActions = isLastReply && !!markdown && !isNotReady;
const messageClasses = classMap({
'assistant-message-container': isAssistantMessage,
});
- const { attachments, role, content, userId, userName, avatarUrl } =
- message;
+ if (status === 'loading' && isLastReply) {
+ return html``;
+ }
return html`
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
@@ -451,7 +471,7 @@ export class AIChatBlockPeekView extends LitElement {
? html`
{
- if (this._chatMessagesContainer) {
- this._chatMessagesContainer.scrollTop =
- this._chatMessagesContainer.scrollHeight;
- }
- });
+ this._scrollToEnd();
+ }
+
+ protected override updated(changedProperties: PropertyValues) {
+ if (
+ changedProperties.has('chatContext') &&
+ (this.chatContext.status === 'loading' ||
+ this.chatContext.status === 'error' ||
+ this.chatContext.status === 'success')
+ ) {
+ setTimeout(this._scrollToEnd, 500);
+ }
+
+ if (
+ changedProperties.has('chatContext') &&
+ this.chatContext.status === 'transmitting'
+ ) {
+ this._throttledScrollToEnd();
+ }
}
override render() {
@@ -529,6 +561,15 @@ export class AIChatBlockPeekView extends LitElement {
const { messages: currentChatMessages } = chatContext;
return html`
+
${this.CurrentMessages(currentChatMessages)}
-