mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 01:49:51 +08:00
fix(core): artifact rendering issue in standalone ai chat panel (#13164)
#### PR Dependency Tree * **PR #13164** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved integration of workspace context into AI chat, enabling more responsive interactions when clicking document links within chat messages. * Enhanced document opening experience from chat by reacting to link clicks and providing direct access to related documents. * **Refactor** * Streamlined notification handling and workspace context management within chat-related components for better maintainability and performance. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -3,8 +3,11 @@ import type { AppThemeService } from '@affine/core/modules/theme';
|
|||||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import {
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
type BlockStdScope,
|
||||||
|
type EditorHost,
|
||||||
|
ShadowlessElement,
|
||||||
|
} from '@blocksuite/affine/std';
|
||||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||||
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||||
import type { Signal } from '@preact/signals-core';
|
import type { Signal } from '@preact/signals-core';
|
||||||
@@ -37,6 +40,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host: EditorHost | null | undefined;
|
accessor host: EditorHost | null | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor std: BlockStdScope | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor item!: ChatMessage;
|
accessor item!: ChatMessage;
|
||||||
|
|
||||||
@@ -124,6 +130,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
|||||||
private renderStreamObjects(answer: StreamObject[]) {
|
private renderStreamObjects(answer: StreamObject[]) {
|
||||||
return html`<chat-content-stream-objects
|
return html`<chat-content-stream-objects
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
|
.std=${this.std}
|
||||||
.answer=${answer}
|
.answer=${answer}
|
||||||
.state=${this.state}
|
.state=${this.state}
|
||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
|
|||||||
+9
-2
@@ -6,8 +6,11 @@ import type {
|
|||||||
CopilotChatHistoryFragment,
|
CopilotChatHistoryFragment,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import {
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
type BlockStdScope,
|
||||||
|
type EditorHost,
|
||||||
|
ShadowlessElement,
|
||||||
|
} from '@blocksuite/affine/std';
|
||||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||||
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||||
import { type Signal } from '@preact/signals-core';
|
import { type Signal } from '@preact/signals-core';
|
||||||
@@ -127,6 +130,9 @@ export class AIChatContent extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host: EditorHost | null | undefined;
|
accessor host: EditorHost | null | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor std: BlockStdScope | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor session!: CopilotChatHistoryFragment | null | undefined;
|
accessor session!: CopilotChatHistoryFragment | null | undefined;
|
||||||
|
|
||||||
@@ -400,6 +406,7 @@ export class AIChatContent extends SignalWatcher(
|
|||||||
})}
|
})}
|
||||||
${ref(this.chatMessagesRef)}
|
${ref(this.chatMessagesRef)}
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
|
.std=${this.std}
|
||||||
.workspaceId=${this.workspaceId}
|
.workspaceId=${this.workspaceId}
|
||||||
.docId=${this.docId}
|
.docId=${this.docId}
|
||||||
.session=${this.session}
|
.session=${this.session}
|
||||||
|
|||||||
+9
-2
@@ -6,8 +6,11 @@ import {
|
|||||||
type FeatureFlagService,
|
type FeatureFlagService,
|
||||||
type NotificationService,
|
type NotificationService,
|
||||||
} from '@blocksuite/affine/shared/services';
|
} from '@blocksuite/affine/shared/services';
|
||||||
import type { EditorHost } from '@blocksuite/affine/std';
|
import {
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
type BlockStdScope,
|
||||||
|
type EditorHost,
|
||||||
|
ShadowlessElement,
|
||||||
|
} from '@blocksuite/affine/std';
|
||||||
import type { BaseSelection, ExtensionType } from '@blocksuite/affine/store';
|
import type { BaseSelection, ExtensionType } from '@blocksuite/affine/store';
|
||||||
import { ArrowDownBigIcon as ArrowDownIcon } from '@blocksuite/icons/lit';
|
import { ArrowDownBigIcon as ArrowDownIcon } from '@blocksuite/icons/lit';
|
||||||
import type { Signal } from '@preact/signals-core';
|
import type { Signal } from '@preact/signals-core';
|
||||||
@@ -160,6 +163,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host: EditorHost | null | undefined;
|
accessor host: EditorHost | null | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor std: BlockStdScope | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor workspaceId!: string;
|
accessor workspaceId!: string;
|
||||||
|
|
||||||
@@ -318,6 +324,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
|
|||||||
} else if (isChatMessage(item) && item.role === 'assistant') {
|
} else if (isChatMessage(item) && item.role === 'assistant') {
|
||||||
return html`<chat-message-assistant
|
return html`<chat-message-assistant
|
||||||
.host=${this.host}
|
.host=${this.host}
|
||||||
|
.std=${this.std}
|
||||||
.session=${this.session}
|
.session=${this.session}
|
||||||
.item=${item}
|
.item=${item}
|
||||||
.isLast=${isLast}
|
.isLast=${isLast}
|
||||||
|
|||||||
+12
-5
@@ -1,7 +1,11 @@
|
|||||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||||
import type { ColorScheme } from '@blocksuite/affine/model';
|
import type { ColorScheme } from '@blocksuite/affine/model';
|
||||||
import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
|
import {
|
||||||
|
type BlockStdScope,
|
||||||
|
type EditorHost,
|
||||||
|
ShadowlessElement,
|
||||||
|
} from '@blocksuite/affine/std';
|
||||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||||
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
import type { NotificationService } from '@blocksuite/affine-shared/services';
|
||||||
import type { Signal } from '@preact/signals-core';
|
import type { Signal } from '@preact/signals-core';
|
||||||
@@ -29,6 +33,9 @@ export class ChatContentStreamObjects extends WithDisposable(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host: EditorHost | null | undefined;
|
accessor host: EditorHost | null | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor std: BlockStdScope | null | undefined;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor state: AffineAIPanelState = 'finished';
|
accessor state: AffineAIPanelState = 'finished';
|
||||||
|
|
||||||
@@ -70,7 +77,7 @@ export class ChatContentStreamObjects extends WithDisposable(
|
|||||||
case 'doc_compose':
|
case 'doc_compose':
|
||||||
return html`
|
return html`
|
||||||
<doc-compose-tool
|
<doc-compose-tool
|
||||||
.std=${this.host?.std}
|
.std=${this.std || this.host?.std}
|
||||||
.data=${streamObject}
|
.data=${streamObject}
|
||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
.theme=${this.theme}
|
.theme=${this.theme}
|
||||||
@@ -80,7 +87,7 @@ export class ChatContentStreamObjects extends WithDisposable(
|
|||||||
case 'code_artifact':
|
case 'code_artifact':
|
||||||
return html`
|
return html`
|
||||||
<code-artifact-tool
|
<code-artifact-tool
|
||||||
.std=${this.host?.std}
|
.std=${this.std || this.host?.std}
|
||||||
.data=${streamObject}
|
.data=${streamObject}
|
||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
></code-artifact-tool>
|
></code-artifact-tool>
|
||||||
@@ -125,7 +132,7 @@ export class ChatContentStreamObjects extends WithDisposable(
|
|||||||
case 'doc_compose':
|
case 'doc_compose':
|
||||||
return html`
|
return html`
|
||||||
<doc-compose-tool
|
<doc-compose-tool
|
||||||
.std=${this.host?.std}
|
.std=${this.std || this.host?.std}
|
||||||
.data=${streamObject}
|
.data=${streamObject}
|
||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
.theme=${this.theme}
|
.theme=${this.theme}
|
||||||
@@ -135,7 +142,7 @@ export class ChatContentStreamObjects extends WithDisposable(
|
|||||||
case 'code_artifact':
|
case 'code_artifact':
|
||||||
return html`
|
return html`
|
||||||
<code-artifact-tool
|
<code-artifact-tool
|
||||||
.std=${this.host?.std}
|
.std=${this.std || this.host?.std}
|
||||||
.data=${streamObject}
|
.data=${streamObject}
|
||||||
.width=${this.width}
|
.width=${this.width}
|
||||||
.theme=${this.theme}
|
.theme=${this.theme}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getEmbedLinkedDocIcons } from '@blocksuite/affine/blocks/embed-doc';
|
|||||||
import { LoadingIcon } from '@blocksuite/affine/components/icons';
|
import { LoadingIcon } from '@blocksuite/affine/components/icons';
|
||||||
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
|
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
|
||||||
import type { ColorScheme } from '@blocksuite/affine/model';
|
import type { ColorScheme } from '@blocksuite/affine/model';
|
||||||
import { NotificationProvider } from '@blocksuite/affine/shared/services';
|
|
||||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc';
|
import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc';
|
||||||
import { CopyIcon, PageIcon, ToolIcon } from '@blocksuite/icons/lit';
|
import { CopyIcon, PageIcon, ToolIcon } from '@blocksuite/icons/lit';
|
||||||
@@ -110,8 +109,6 @@ export class DocComposeTool extends ArtifactTool<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override getPreviewContent() {
|
protected override getPreviewContent() {
|
||||||
if (!this.std) return html``;
|
|
||||||
const std = this.std;
|
|
||||||
const resultData = this.data;
|
const resultData = this.data;
|
||||||
const title = this.data.args.title;
|
const title = this.data.args.title;
|
||||||
const result = resultData.type === 'tool-result' ? resultData.result : null;
|
const result = resultData.type === 'tool-result' ? resultData.result : null;
|
||||||
@@ -122,8 +119,7 @@ export class DocComposeTool extends ArtifactTool<
|
|||||||
${successResult
|
${successResult
|
||||||
? html`<text-renderer
|
? html`<text-renderer
|
||||||
.answer=${successResult.markdown}
|
.answer=${successResult.markdown}
|
||||||
.host=${std.host}
|
.schema=${this.std?.store.schema}
|
||||||
.schema=${std.store.schema}
|
|
||||||
.options=${{
|
.options=${{
|
||||||
customHeading: true,
|
customHeading: true,
|
||||||
extensions: getCustomPageEditorBlockSpecs(),
|
extensions: getCustomPageEditorBlockSpecs(),
|
||||||
@@ -161,7 +157,6 @@ export class DocComposeTool extends ArtifactTool<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspace = std.store.workspace;
|
const workspace = std.store.workspace;
|
||||||
const notificationService = std.get(NotificationProvider);
|
|
||||||
const refNodeSlots = std.getOptional(RefNodeSlotsProvider);
|
const refNodeSlots = std.getOptional(RefNodeSlotsProvider);
|
||||||
const docId = await MarkdownTransformer.importMarkdownToDoc({
|
const docId = await MarkdownTransformer.importMarkdownToDoc({
|
||||||
collection: workspace,
|
collection: workspace,
|
||||||
@@ -171,7 +166,7 @@ export class DocComposeTool extends ArtifactTool<
|
|||||||
extensions: getStoreManager().config.init().value.get('store'),
|
extensions: getStoreManager().config.init().value.get('store'),
|
||||||
});
|
});
|
||||||
if (docId) {
|
if (docId) {
|
||||||
const open = await notificationService.confirm({
|
const open = await this.notificationService.confirm({
|
||||||
title: 'Open the doc you just created',
|
title: 'Open the doc you just created',
|
||||||
message: 'Doc saved successfully! Would you like to open it now?',
|
message: 'Doc saved successfully! Would you like to open it now?',
|
||||||
cancelText: 'Cancel',
|
cancelText: 'Cancel',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages';
|
import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages';
|
||||||
import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
||||||
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
|
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
|
||||||
|
import { getViewManager } from '@affine/core/blocksuite/manager/view';
|
||||||
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
|
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
|
||||||
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
||||||
import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
|
import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
|
||||||
@@ -27,8 +28,12 @@ import {
|
|||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
} from '@affine/core/modules/workbench';
|
} from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
|
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
|
||||||
|
import { BlockStdScope } from '@blocksuite/affine/std';
|
||||||
|
import type { Workspace } from '@blocksuite/affine/store';
|
||||||
import { type Signal, signal } from '@preact/signals-core';
|
import { type Signal, signal } from '@preact/signals-core';
|
||||||
import { useFramework, useService } from '@toeverything/infra';
|
import { useFramework, useService } from '@toeverything/infra';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
@@ -51,6 +56,26 @@ function useCopilotClient() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMockStd(workspace: Workspace) {
|
||||||
|
workspace.meta.initialize();
|
||||||
|
const store = workspace.createDoc().getStore();
|
||||||
|
const std = new BlockStdScope({
|
||||||
|
store,
|
||||||
|
extensions: [...getViewManager().config.init().value.get('page')],
|
||||||
|
});
|
||||||
|
std.render();
|
||||||
|
return std;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMockStd() {
|
||||||
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
const std = useMemo(() => {
|
||||||
|
if (!workspace) return null;
|
||||||
|
return createMockStd(workspace.docCollection);
|
||||||
|
}, [workspace]);
|
||||||
|
return std;
|
||||||
|
}
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const framework = useFramework();
|
const framework = useFramework();
|
||||||
const [isBodyProvided, setIsBodyProvided] = useState(false);
|
const [isBodyProvided, setIsBodyProvided] = useState(false);
|
||||||
@@ -145,6 +170,7 @@ export const Component = () => {
|
|||||||
|
|
||||||
const confirmModal = useConfirmModal();
|
const confirmModal = useConfirmModal();
|
||||||
const specs = useAISpecs();
|
const specs = useAISpecs();
|
||||||
|
const mockStd = useMockStd();
|
||||||
|
|
||||||
// init or update ai-chat-content
|
// init or update ai-chat-content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -161,6 +187,7 @@ export const Component = () => {
|
|||||||
content.session = currentSession;
|
content.session = currentSession;
|
||||||
content.workspaceId = workspaceId;
|
content.workspaceId = workspaceId;
|
||||||
content.extensions = specs;
|
content.extensions = specs;
|
||||||
|
content.std = mockStd;
|
||||||
content.docDisplayConfig = docDisplayConfig;
|
content.docDisplayConfig = docDisplayConfig;
|
||||||
content.searchMenuConfig = searchMenuConfig;
|
content.searchMenuConfig = searchMenuConfig;
|
||||||
content.networkSearchConfig = networkSearchConfig;
|
content.networkSearchConfig = networkSearchConfig;
|
||||||
@@ -192,6 +219,7 @@ export const Component = () => {
|
|||||||
docDisplayConfig,
|
docDisplayConfig,
|
||||||
framework,
|
framework,
|
||||||
isBodyProvided,
|
isBodyProvided,
|
||||||
|
mockStd,
|
||||||
networkSearchConfig,
|
networkSearchConfig,
|
||||||
reasoningConfig,
|
reasoningConfig,
|
||||||
searchMenuConfig,
|
searchMenuConfig,
|
||||||
@@ -260,37 +288,21 @@ export const Component = () => {
|
|||||||
status,
|
status,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// restore pinned session
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chatContent) return;
|
const refNodeSlots = mockStd?.getOptional(RefNodeSlotsProvider);
|
||||||
|
if (!refNodeSlots) return;
|
||||||
const controller = new AbortController();
|
const sub = refNodeSlots.docLinkClicked.subscribe(event => {
|
||||||
const signal = controller.signal;
|
const { workbench } = framework.get(WorkbenchService);
|
||||||
client
|
workbench.openDoc({
|
||||||
.getSessions(
|
docId: event.pageId,
|
||||||
workspaceId,
|
mode: event.params?.mode,
|
||||||
{},
|
blockIds: event.params?.blockIds,
|
||||||
undefined,
|
elementIds: event.params?.elementIds,
|
||||||
{ pinned: true, limit: 1 },
|
refreshKey: nanoid(),
|
||||||
signal
|
});
|
||||||
)
|
});
|
||||||
.then(sessions => {
|
return () => sub.unsubscribe();
|
||||||
if (!Array.isArray(sessions)) return;
|
}, [framework, mockStd]);
|
||||||
const session = sessions[0];
|
|
||||||
if (!session) return;
|
|
||||||
setCurrentSession(session);
|
|
||||||
if (chatContent) {
|
|
||||||
chatContent.session = session;
|
|
||||||
chatContent.reloadSession();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
// abort the request
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
};
|
|
||||||
}, [chatContent, client, workspaceId]);
|
|
||||||
|
|
||||||
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
||||||
if (node) {
|
if (node) {
|
||||||
|
|||||||
Reference in New Issue
Block a user