mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
feat: cleanup chat panel (#14259)
#### PR Dependency Tree * **PR #14258** * **PR #14259** 👈 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 * **Refactor** * Split AI initialization into separate editor, app, and shared registries; removed legacy chat-panel and replaced it with a component-based editor chat, updating wiring and public exports. * Propagated server/subscription/model services into chat/playground components and improved session lifecycle and UI composition. * **Tests** * Added tests for AI effect registration and chat session resolution; extended DOM/test utilities and assertions. * **Chores** * Added happy-dom for runtime and test environments. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -109,6 +109,7 @@
|
||||
"@types/semver": "^7",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"happy-dom": "^20.3.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
|
||||
30
packages/frontend/core/src/__tests__/ai/effects.spec.ts
Normal file
30
packages/frontend/core/src/__tests__/ai/effects.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
appEffectElementTags,
|
||||
editorEffectElementTags,
|
||||
sharedEffectElementTags,
|
||||
} from '@affine/core/blocksuite/ai/effects/registry';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
describe('ai effects registration split', () => {
|
||||
const editorTags = new Set<string>([
|
||||
...editorEffectElementTags,
|
||||
...sharedEffectElementTags,
|
||||
]);
|
||||
const appTags = new Set<string>([
|
||||
...appEffectElementTags,
|
||||
...sharedEffectElementTags,
|
||||
]);
|
||||
|
||||
test('registerAIEditorEffects skips app-only elements', () => {
|
||||
expect(editorTags.has('affine-ai-chat')).toBe(true);
|
||||
expect(editorTags.has('chat-panel')).toBe(false);
|
||||
expect(editorTags.has('text-renderer')).toBe(true);
|
||||
});
|
||||
|
||||
test('registerAIAppEffects skips editor-only elements', () => {
|
||||
expect(appTags.has('ai-chat-content')).toBe(true);
|
||||
expect(appTags.has('chat-panel')).toBe(false);
|
||||
expect(appTags.has('affine-ai-chat')).toBe(false);
|
||||
expect(appTags.has('text-renderer')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,23 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import '@blocksuite/affine-shared/test-utils';
|
||||
|
||||
import { getInternalStoreExtensions } from '@blocksuite/affine/extensions/store';
|
||||
import { StoreExtensionManager } from '@blocksuite/affine-ext-loader';
|
||||
import { createAffineTemplate } from '@blocksuite/affine-shared/test-utils';
|
||||
import type { Store } from '@blocksuite/store';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { applyPatchToDoc } from '../../../../blocksuite/ai/utils/apply-model/apply-patch-to-doc';
|
||||
import type { PatchOp } from '../../../../blocksuite/ai/utils/apply-model/markdown-diff';
|
||||
|
||||
declare module 'vitest' {
|
||||
interface Assertion<T = any> {
|
||||
toEqualDoc(expected: Store, options?: { compareId?: boolean }): T;
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new StoreExtensionManager(getInternalStoreExtensions());
|
||||
const { affine } = createAffineTemplate(manager.get('store'));
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { property, state } from 'lit/decorators.js';
|
||||
|
||||
import { type ChatAction } from '../../components/ai-chat-messages';
|
||||
import { createTextRenderer } from '../../components/text-renderer';
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../const';
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions';
|
||||
|
||||
const icons: Record<string, TemplateResult<1>> = {
|
||||
'Fix spelling for it': DoneIcon(),
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { type NotificationService } from '@blocksuite/affine/shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { ExtensionType, Store } from '@blocksuite/affine/store';
|
||||
import { CenterPeekIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { SearchMenuConfig } from '../components/ai-chat-add-context';
|
||||
import type { DocDisplayConfig } from '../components/ai-chat-chips';
|
||||
import type {
|
||||
AIPlaygroundConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../components/ai-chat-input';
|
||||
import type { ChatStatus } from '../components/ai-chat-messages';
|
||||
import { createPlaygroundModal } from '../components/playground/modal';
|
||||
import type { AppSidebarConfig } from './chat-config';
|
||||
|
||||
export class AIChatPanelTitle extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.ai-chat-panel-title {
|
||||
background: var(--affine-background-primary-color);
|
||||
position: relative;
|
||||
padding: 8px var(--h-padding, 16px);
|
||||
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);
|
||||
}
|
||||
|
||||
.chat-panel-title-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
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')};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor doc!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor playgroundConfig!: AIPlaygroundConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor appSidebarConfig!: AppSidebarConfig;
|
||||
|
||||
@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 serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineThemeService!: AppThemeService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor session!: CopilotChatHistoryFragment | null | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor status!: ChatStatus;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor embeddingProgress: [number, number] = [0, 0];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor newSession!: () => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor togglePin!: () => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor openSession!: (sessionId: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor openDoc!: (docId: string, sessionId: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor deleteSession!: (session: BlockSuitePresets.AIRecentSession) => void;
|
||||
|
||||
private readonly openPlayground = () => {
|
||||
const playgroundContent = html`
|
||||
<playground-content
|
||||
.host=${this.host}
|
||||
.doc=${this.doc}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.playgroundConfig=${this.playgroundConfig}
|
||||
.appSidebarConfig=${this.appSidebarConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
></playground-content>
|
||||
`;
|
||||
|
||||
createPlaygroundModal(playgroundContent, 'AI Playground');
|
||||
};
|
||||
|
||||
override render() {
|
||||
const [done, total] = this.embeddingProgress;
|
||||
const isEmbedding = total > 0 && done < total;
|
||||
|
||||
return html`
|
||||
<div class="ai-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-chat-toolbar
|
||||
.session=${this.session}
|
||||
.workspaceId=${this.doc.workspace.id}
|
||||
.docId=${this.doc.id}
|
||||
.status=${this.status}
|
||||
.onNewSession=${this.newSession}
|
||||
.onTogglePin=${this.togglePin}
|
||||
.onOpenSession=${this.openSession}
|
||||
.onOpenDoc=${this.openDoc}
|
||||
.onSessionDelete=${this.deleteSession}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.notificationService=${this.notificationService}
|
||||
></ai-chat-toolbar>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
import type {
|
||||
AIDraftService,
|
||||
AIToolsConfigService,
|
||||
} from '@affine/core/modules/ai-button';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type {
|
||||
ContextEmbedStatus,
|
||||
CopilotChatHistoryFragment,
|
||||
UpdateChatSessionInput,
|
||||
} from '@affine/graphql';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { type NotificationService } from '@blocksuite/affine/shared/services';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { ExtensionType, Store } from '@blocksuite/affine/store';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import { css, html, type PropertyValues } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
|
||||
import { AffineIcon } from '../_common/icons';
|
||||
import type { SearchMenuConfig } from '../components/ai-chat-add-context';
|
||||
import type { DocDisplayConfig } from '../components/ai-chat-chips';
|
||||
import type { ChatContextValue } from '../components/ai-chat-content';
|
||||
import type {
|
||||
AIPlaygroundConfig,
|
||||
AIReasoningConfig,
|
||||
} from '../components/ai-chat-input';
|
||||
import type { ChatStatus } from '../components/ai-chat-messages';
|
||||
import { AIProvider } from '../provider';
|
||||
import type { AppSidebarConfig } from './chat-config';
|
||||
|
||||
export class ChatPanel extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
chat-panel {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
|
||||
.chat-panel-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ai-chat-content {
|
||||
height: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.chat-loading-container {
|
||||
position: relative;
|
||||
padding: 44px 0 166px 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-loading {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chat-loading-title {
|
||||
font-weight: 600;
|
||||
font-size: var(--affine-font-sm);
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor doc!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor playgroundConfig!: AIPlaygroundConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor appSidebarConfig!: AppSidebarConfig;
|
||||
|
||||
@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 serverService!: ServerService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkbenchService!: WorkbenchService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineThemeService!: AppThemeService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor notificationService!: NotificationService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiDraftService!: AIDraftService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor peekViewService!: PeekViewService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor subscriptionService!: SubscriptionService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiModelService!: AIModelService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onAISubscribe!: () => Promise<void>;
|
||||
|
||||
@state()
|
||||
accessor session: CopilotChatHistoryFragment | null | undefined;
|
||||
|
||||
@state()
|
||||
accessor embeddingProgress: [number, number] = [0, 0];
|
||||
|
||||
@state()
|
||||
accessor status: ChatStatus = 'idle';
|
||||
|
||||
private isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
||||
|
||||
private sidebarWidth: Signal<number | undefined> = signal(undefined);
|
||||
|
||||
private hasPinned = false;
|
||||
|
||||
private get isInitialized() {
|
||||
return this.session !== undefined;
|
||||
}
|
||||
|
||||
private readonly getSessionIdFromUrl = () => {
|
||||
if (this.affineWorkbenchService) {
|
||||
const { workbench } = this.affineWorkbenchService;
|
||||
const location = workbench.location$.value;
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const sessionId = searchParams.get('sessionId');
|
||||
if (sessionId) {
|
||||
workbench.activeView$.value.updateQueryString(
|
||||
{ sessionId: undefined },
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private readonly setSession = (
|
||||
session: CopilotChatHistoryFragment | null | undefined
|
||||
) => {
|
||||
this.session = session ?? null;
|
||||
};
|
||||
|
||||
private readonly initSession = async () => {
|
||||
if (!AIProvider.session) {
|
||||
return;
|
||||
}
|
||||
const sessionId = this.getSessionIdFromUrl();
|
||||
const pinSessions = await AIProvider.session.getSessions(
|
||||
this.doc.workspace.id,
|
||||
undefined,
|
||||
{ pinned: true, limit: 1 }
|
||||
);
|
||||
|
||||
if (Array.isArray(pinSessions) && pinSessions[0]) {
|
||||
// pinned session
|
||||
this.session = pinSessions[0];
|
||||
} else if (sessionId) {
|
||||
// sessionId from url
|
||||
const session = await AIProvider.session.getSession(
|
||||
this.doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
this.setSession(session);
|
||||
} else {
|
||||
// latest doc session
|
||||
const docSessions = await AIProvider.session.getSessions(
|
||||
this.doc.workspace.id,
|
||||
this.doc.id,
|
||||
{ action: false, fork: false, limit: 1 }
|
||||
);
|
||||
// sessions is descending ordered by updatedAt
|
||||
// the first item is the latest session
|
||||
const session = docSessions?.[0];
|
||||
this.setSession(session);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly createSession = async (
|
||||
options: Partial<BlockSuitePresets.AICreateSessionOptions> = {}
|
||||
) => {
|
||||
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',
|
||||
reuseLatestChat: false,
|
||||
...options,
|
||||
});
|
||||
if (sessionId) {
|
||||
const session = await AIProvider.session?.getSession(
|
||||
this.doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
this.setSession(session);
|
||||
}
|
||||
return this.session;
|
||||
};
|
||||
|
||||
private readonly deleteSession = async (
|
||||
session: BlockSuitePresets.AIRecentSession
|
||||
) => {
|
||||
if (!AIProvider.histories) {
|
||||
return;
|
||||
}
|
||||
const confirm = await this.notificationService.confirm({
|
||||
title: 'Delete this history?',
|
||||
message:
|
||||
'Do you want to delete this AI conversation history? Once deleted, it cannot be recovered.',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
});
|
||||
if (confirm) {
|
||||
await AIProvider.histories.cleanup(
|
||||
session.workspaceId,
|
||||
session.docId || undefined,
|
||||
[session.sessionId]
|
||||
);
|
||||
if (session.sessionId === this.session?.sessionId) {
|
||||
this.newSession();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private readonly updateSession = async (options: UpdateChatSessionInput) => {
|
||||
await AIProvider.session?.updateSession(options);
|
||||
const session = await AIProvider.session?.getSession(
|
||||
this.doc.workspace.id,
|
||||
options.sessionId
|
||||
);
|
||||
this.setSession(session);
|
||||
};
|
||||
|
||||
private readonly newSession = () => {
|
||||
this.resetPanel();
|
||||
requestAnimationFrame(() => {
|
||||
this.session = null;
|
||||
});
|
||||
};
|
||||
|
||||
private readonly openSession = async (sessionId: string) => {
|
||||
if (this.session?.sessionId === sessionId) {
|
||||
return;
|
||||
}
|
||||
this.resetPanel();
|
||||
const session = await AIProvider.session?.getSession(
|
||||
this.doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
this.setSession(session);
|
||||
};
|
||||
|
||||
private readonly openDoc = async (docId: string, sessionId: string) => {
|
||||
if (this.doc.id === docId) {
|
||||
if (this.session?.sessionId === sessionId || this.session?.pinned) {
|
||||
return;
|
||||
}
|
||||
await this.openSession(sessionId);
|
||||
} else if (this.affineWorkbenchService) {
|
||||
const { workbench } = this.affineWorkbenchService;
|
||||
if (this.session?.pinned) {
|
||||
workbench.open(`/${docId}`, { at: 'active' });
|
||||
} else {
|
||||
workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private readonly togglePin = async () => {
|
||||
const pinned = !this.session?.pinned;
|
||||
this.hasPinned = true;
|
||||
if (!this.session) {
|
||||
await this.createSession({ pinned });
|
||||
} else {
|
||||
await this.updateSession({
|
||||
sessionId: this.session.sessionId,
|
||||
pinned,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly rebindSession = async () => {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
if (this.session.docId !== this.doc.id) {
|
||||
await this.updateSession({
|
||||
sessionId: this.session.sessionId,
|
||||
docId: this.doc.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly initPanel = async () => {
|
||||
try {
|
||||
if (!this.isSidebarOpen.value) {
|
||||
return;
|
||||
}
|
||||
await this.initSession();
|
||||
this.hasPinned = !!this.session?.pinned;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly resetPanel = () => {
|
||||
this.session = undefined;
|
||||
this.embeddingProgress = [0, 0];
|
||||
this.hasPinned = false;
|
||||
};
|
||||
|
||||
private readonly onEmbeddingProgressChange = (
|
||||
count: Record<ContextEmbedStatus, number>
|
||||
) => {
|
||||
const total = count.finished + count.processing + count.failed;
|
||||
this.embeddingProgress = [count.finished, total];
|
||||
};
|
||||
|
||||
private readonly onContextChange = async (
|
||||
context: Partial<ChatContextValue>
|
||||
) => {
|
||||
this.status = context.status ?? 'idle';
|
||||
if (context.status === 'success') {
|
||||
await this.rebindSession();
|
||||
}
|
||||
};
|
||||
|
||||
protected override updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has('doc')) {
|
||||
if (this.session?.pinned) {
|
||||
return;
|
||||
}
|
||||
this.resetPanel();
|
||||
this.initPanel().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.doc) throw new Error('doc is required');
|
||||
|
||||
this._disposables.add(
|
||||
AIProvider.slots.userInfo.subscribe(() => {
|
||||
this.resetPanel();
|
||||
this.initPanel().catch(console.error);
|
||||
})
|
||||
);
|
||||
|
||||
const isOpen = this.appSidebarConfig.isOpen();
|
||||
this.isSidebarOpen = isOpen.signal;
|
||||
this._disposables.add(isOpen.cleanup);
|
||||
|
||||
const width = this.appSidebarConfig.getWidth();
|
||||
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);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.isInitialized) {
|
||||
return html`<div class="chat-loading-container">
|
||||
<div class="chat-loading">
|
||||
${AffineIcon('var(--affine-icon-secondary)')}
|
||||
<div class="chat-loading-title">
|
||||
<span> AFFiNE AI is loading history... </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html`<div class="chat-panel-container">
|
||||
<ai-chat-panel-title
|
||||
.host=${this.host}
|
||||
.doc=${this.doc}
|
||||
.playgroundConfig=${this.playgroundConfig}
|
||||
.appSidebarConfig=${this.appSidebarConfig}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.session=${this.session}
|
||||
.status=${this.status}
|
||||
.embeddingProgress=${this.embeddingProgress}
|
||||
.newSession=${this.newSession}
|
||||
.togglePin=${this.togglePin}
|
||||
.openSession=${this.openSession}
|
||||
.openDoc=${this.openDoc}
|
||||
.deleteSession=${this.deleteSession}
|
||||
></ai-chat-panel-title>
|
||||
${keyed(
|
||||
this.hasPinned ? this.session?.sessionId : this.doc.id,
|
||||
html`<ai-chat-content
|
||||
.host=${this.host}
|
||||
.session=${this.session}
|
||||
.createSession=${this.createSession}
|
||||
.workspaceId=${this.doc.workspace.id}
|
||||
.docId=${this.doc.id}
|
||||
.reasoningConfig=${this.reasoningConfig}
|
||||
.searchMenuConfig=${this.searchMenuConfig}
|
||||
.docDisplayConfig=${this.docDisplayConfig}
|
||||
.extensions=${this.extensions}
|
||||
.serverService=${this.serverService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiDraftService=${this.aiDraftService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.peekViewService=${this.peekViewService}
|
||||
.subscriptionService=${this.subscriptionService}
|
||||
.aiModelService=${this.aiModelService}
|
||||
.onAISubscribe=${this.onAISubscribe}
|
||||
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
|
||||
.onContextChange=${this.onContextChange}
|
||||
.width=${this.sidebarWidth}
|
||||
.onOpenDoc=${this.openDoc}
|
||||
></ai-chat-content>`
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'chat-panel': ChatPanel;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { type ChatAction } from '../../components/ai-chat-messages';
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../const';
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions';
|
||||
|
||||
export class ChatMessageAction extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
|
||||
@@ -28,9 +28,9 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
||||
import { type AIChatParams, AIProvider } from '../../provider/ai-provider';
|
||||
import { extractSelectedContent } from '../../utils/extract';
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions';
|
||||
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||
import type { AIReasoningConfig } from '../ai-chat-input';
|
||||
|
||||
@@ -19,12 +19,12 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import { AffineIcon } from '../../_common/icons';
|
||||
import { AIPreloadConfig } from '../../chat-panel/preload-config';
|
||||
import { type AIError, AIProvider, UnauthorizedError } from '../../provider';
|
||||
import { mergeStreamObjects } from '../../utils/stream-objects';
|
||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||
import { type ChatContextValue } from '../ai-chat-content/type';
|
||||
import type { AIReasoningConfig } from '../ai-chat-input';
|
||||
import { AIPreloadConfig } from './preload-config';
|
||||
import {
|
||||
type HistoryMessage,
|
||||
isChatAction,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
SendIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
|
||||
import { AIProvider } from '../provider/ai-provider.js';
|
||||
import { AIProvider } from '../../provider/ai-provider.js';
|
||||
import completeWritingWithAI from './templates/completeWritingWithAI.zip';
|
||||
import freelyCommunicateWithAI from './templates/freelyCommunicateWithAI.zip';
|
||||
import readAforeign from './templates/readAforeign.zip';
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
@@ -20,8 +24,8 @@ import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
import type { AppSidebarConfig } from '../../chat-panel/chat-config';
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../../chat-panel/const';
|
||||
import { AIProvider } from '../../provider';
|
||||
import { HISTORY_IMAGE_ACTIONS } from '../../utils/history-image-actions';
|
||||
import type { SearchMenuConfig } from '../ai-chat-add-context';
|
||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||
import type { ChatContextValue } from '../ai-chat-content';
|
||||
@@ -179,6 +183,12 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor subscriptionService!: SubscriptionService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiModelService!: AIModelService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onAISubscribe: (() => Promise<void>) | undefined;
|
||||
|
||||
@@ -373,6 +383,8 @@ export class PlaygroundChat extends SignalWatcher(
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.subscriptionService=${this.subscriptionService}
|
||||
.aiModelService=${this.aiModelService}
|
||||
.onAISubscribe=${this.onAISubscribe}
|
||||
></ai-chat-composer>
|
||||
</div>`;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||
import type { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import type {
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { AppThemeService } from '@affine/core/modules/theme';
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
@@ -93,6 +98,15 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor subscriptionService!: SubscriptionService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor aiModelService!: AIModelService;
|
||||
|
||||
@state()
|
||||
accessor sessions: CopilotChatHistoryFragment[] = [];
|
||||
|
||||
@@ -351,6 +365,10 @@ export class PlaygroundContent extends SignalWatcher(
|
||||
.affineThemeService=${this.affineThemeService}
|
||||
.notificationService=${this.notificationService}
|
||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||
.affineWorkspaceDialogService=${this
|
||||
.affineWorkspaceDialogService}
|
||||
.subscriptionService=${this.subscriptionService}
|
||||
.aiModelService=${this.aiModelService}
|
||||
.addChat=${this.addChat}
|
||||
></playground-chat>
|
||||
</div>
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
import { effects as tooltipEffects } from '@blocksuite/affine-components/tooltip';
|
||||
|
||||
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 {
|
||||
AIChatBlockMessage,
|
||||
AIChatBlockMessages,
|
||||
} from './blocks/ai-chat-block/components/ai-chat-messages';
|
||||
import {
|
||||
ChatImage,
|
||||
ChatImages,
|
||||
} from './blocks/ai-chat-block/components/chat-images';
|
||||
import { ImagePlaceholder } from './blocks/ai-chat-block/components/image-placeholder';
|
||||
import { UserInfo } from './blocks/ai-chat-block/components/user-info';
|
||||
import { ChatPanel } from './chat-panel';
|
||||
import { ActionWrapper } from './chat-panel/actions/action-wrapper';
|
||||
import { ActionImage } from './chat-panel/actions/image';
|
||||
import { ActionImageToText } from './chat-panel/actions/image-to-text';
|
||||
import { ActionMakeReal } from './chat-panel/actions/make-real';
|
||||
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 { AIChatPanelTitle } from './chat-panel/ai-title';
|
||||
import { ChatMessageAction } from './chat-panel/message/action';
|
||||
import { ChatMessageAssistant } from './chat-panel/message/assistant';
|
||||
import { ChatMessageUser } from './chat-panel/message/user';
|
||||
import { ChatPanelSplitView } from './chat-panel/split-view';
|
||||
import { ArtifactSkeleton } from './components/ai-artifact-skeleton';
|
||||
import { AIChatAddContext } from './components/ai-chat-add-context';
|
||||
import { ChatPanelAddPopover } from './components/ai-chat-chips/add-popover';
|
||||
import { ChatPanelAttachmentChip } from './components/ai-chat-chips/attachment-chip';
|
||||
import { ChatPanelCandidatesPopover } from './components/ai-chat-chips/candidates-popover';
|
||||
import { ChatPanelChips } from './components/ai-chat-chips/chat-panel-chips';
|
||||
import { ChatPanelChip } from './components/ai-chat-chips/chip';
|
||||
import { ChatPanelCollectionChip } from './components/ai-chat-chips/collection-chip';
|
||||
import { ChatPanelDocChip } from './components/ai-chat-chips/doc-chip';
|
||||
import { ChatPanelFileChip } from './components/ai-chat-chips/file-chip';
|
||||
import { ChatPanelSelectedChip } from './components/ai-chat-chips/selected-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 { AIChatToolbar, AISessionHistory } from './components/ai-chat-toolbar';
|
||||
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 { ArtifactPreviewPanel } from './components/ai-tools/artifacts-preview-panel';
|
||||
import {
|
||||
CodeArtifactTool,
|
||||
CodeHighlighter,
|
||||
} from './components/ai-tools/code-artifact';
|
||||
import { DocComposeTool } from './components/ai-tools/doc-compose';
|
||||
import { DocEditTool } from './components/ai-tools/doc-edit';
|
||||
import { DocKeywordSearchResult } from './components/ai-tools/doc-keyword-search-result';
|
||||
import { DocReadResult } from './components/ai-tools/doc-read-result';
|
||||
import { DocSemanticSearchResult } from './components/ai-tools/doc-semantic-search-result';
|
||||
import { DocWriteTool } from './components/ai-tools/doc-write';
|
||||
import { SectionEditTool } from './components/ai-tools/section-edit';
|
||||
import { ToolCallCard } from './components/ai-tools/tool-call-card';
|
||||
import { ToolFailedCard } from './components/ai-tools/tool-failed-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';
|
||||
import { AskAIToolbarButton } from './components/ask-ai-toolbar';
|
||||
import { ChatActionList } from './components/chat-action-list';
|
||||
import { ChatCopyMore } from './components/copy-more';
|
||||
import { ImagePreviewGrid } from './components/image-preview-grid';
|
||||
import { effects as componentPlaygroundEffects } from './components/playground';
|
||||
import { TextRenderer } from './components/text-renderer';
|
||||
import { AIErrorWrapper } from './messages/error';
|
||||
import { AISlidesRenderer } from './messages/slides-renderer';
|
||||
import { AIAnswerWrapper } from './messages/wrapper';
|
||||
import { registerMiniMindmapBlocks } from './mini-mindmap';
|
||||
import { AIChatBlockPeekView } from './peek-view/chat-block-peek-view';
|
||||
import { DateTime } from './peek-view/date-time';
|
||||
import {
|
||||
AFFINE_AI_PANEL_WIDGET,
|
||||
AffineAIPanelWidget,
|
||||
} from './widgets/ai-panel/ai-panel';
|
||||
import {
|
||||
AIPanelAnswer,
|
||||
AIPanelDivider,
|
||||
AIPanelError,
|
||||
AIPanelGenerating,
|
||||
AIPanelInput,
|
||||
} from './widgets/ai-panel/components';
|
||||
import { AIFinishTip } from './widgets/ai-panel/components/finish-tip';
|
||||
import { GeneratingPlaceholder } from './widgets/ai-panel/components/generating-placeholder';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
|
||||
AffineBlockDiffWidgetForBlock,
|
||||
} from './widgets/block-diff/block';
|
||||
import { BlockDiffOptions } from './widgets/block-diff/options';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
|
||||
AffineBlockDiffWidgetForPage,
|
||||
} from './widgets/block-diff/page';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND,
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL,
|
||||
BlockDiffPlayground,
|
||||
BlockDiffPlaygroundModal,
|
||||
} from './widgets/block-diff/playground';
|
||||
import {
|
||||
AFFINE_EDGELESS_COPILOT_WIDGET,
|
||||
EdgelessCopilotWidget,
|
||||
} from './widgets/edgeless-copilot';
|
||||
import { EdgelessCopilotPanel } from './widgets/edgeless-copilot-panel';
|
||||
import { EdgelessCopilotToolbarEntry } from './widgets/edgeless-copilot-panel/toolbar-entry';
|
||||
|
||||
export function registerAIEffects() {
|
||||
registerMiniMindmapBlocks();
|
||||
componentAiItemEffects();
|
||||
componentPlaygroundEffects();
|
||||
tooltipEffects();
|
||||
|
||||
customElements.define('ask-ai-icon', AskAIIcon);
|
||||
customElements.define('ask-ai-button', AskAIButton);
|
||||
customElements.define('ask-ai-toolbar-button', AskAIToolbarButton);
|
||||
customElements.define('ask-ai-panel', AskAIPanel);
|
||||
customElements.define('chat-action-list', ChatActionList);
|
||||
customElements.define('chat-copy-more', ChatCopyMore);
|
||||
customElements.define('image-preview-grid', ImagePreviewGrid);
|
||||
customElements.define('action-wrapper', ActionWrapper);
|
||||
customElements.define('action-image-to-text', ActionImageToText);
|
||||
customElements.define('action-image', ActionImage);
|
||||
customElements.define('action-make-real', ActionMakeReal);
|
||||
customElements.define('action-mindmap', ActionMindmap);
|
||||
customElements.define('action-slides', ActionSlides);
|
||||
customElements.define('action-text', ActionText);
|
||||
customElements.define('ai-loading', AILoading);
|
||||
customElements.define('ai-chat-content', AIChatContent);
|
||||
customElements.define('ai-chat-toolbar', AIChatToolbar);
|
||||
customElements.define('ai-session-history', AISessionHistory);
|
||||
customElements.define('ai-chat-messages', AIChatMessages);
|
||||
customElements.define('chat-panel', ChatPanel);
|
||||
customElements.define('ai-chat-panel-title', AIChatPanelTitle);
|
||||
customElements.define('ai-chat-input', AIChatInput);
|
||||
customElements.define('ai-chat-add-context', AIChatAddContext);
|
||||
customElements.define(
|
||||
'ai-chat-embedding-status-tooltip',
|
||||
AIChatEmbeddingStatusTooltip
|
||||
);
|
||||
customElements.define('ai-chat-composer', AIChatComposer);
|
||||
customElements.define('chat-panel-chips', ChatPanelChips);
|
||||
customElements.define('ai-history-clear', AIHistoryClear);
|
||||
customElements.define('chat-panel-add-popover', ChatPanelAddPopover);
|
||||
customElements.define('chat-input-preference', ChatInputPreference);
|
||||
customElements.define(
|
||||
'chat-panel-candidates-popover',
|
||||
ChatPanelCandidatesPopover
|
||||
);
|
||||
customElements.define('chat-panel-doc-chip', ChatPanelDocChip);
|
||||
customElements.define('chat-panel-file-chip', ChatPanelFileChip);
|
||||
customElements.define('chat-panel-tag-chip', ChatPanelTagChip);
|
||||
customElements.define('chat-panel-collection-chip', ChatPanelCollectionChip);
|
||||
customElements.define('chat-panel-selected-chip', ChatPanelSelectedChip);
|
||||
customElements.define('chat-panel-attachment-chip', ChatPanelAttachmentChip);
|
||||
customElements.define('chat-panel-chip', ChatPanelChip);
|
||||
customElements.define('ai-error-wrapper', AIErrorWrapper);
|
||||
customElements.define('ai-slides-renderer', AISlidesRenderer);
|
||||
customElements.define('ai-answer-wrapper', AIAnswerWrapper);
|
||||
customElements.define('ai-chat-block-peek-view', AIChatBlockPeekView);
|
||||
customElements.define('date-time', DateTime);
|
||||
customElements.define(
|
||||
'affine-edgeless-ai-chat',
|
||||
EdgelessAIChatBlockComponent
|
||||
);
|
||||
customElements.define('affine-ai-chat', AIChatBlockComponent);
|
||||
customElements.define('ai-chat-block-message', AIChatBlockMessage);
|
||||
customElements.define('ai-chat-block-messages', AIChatBlockMessages);
|
||||
customElements.define(
|
||||
'ai-scrollable-text-renderer',
|
||||
AIScrollableTextRenderer
|
||||
);
|
||||
customElements.define('image-placeholder', ImagePlaceholder);
|
||||
customElements.define('chat-image', ChatImage);
|
||||
customElements.define('chat-images', ChatImages);
|
||||
customElements.define('user-info', UserInfo);
|
||||
customElements.define('text-renderer', TextRenderer);
|
||||
|
||||
customElements.define('generating-placeholder', GeneratingPlaceholder);
|
||||
customElements.define('ai-finish-tip', AIFinishTip);
|
||||
customElements.define('ai-panel-divider', AIPanelDivider);
|
||||
customElements.define('ai-panel-answer', AIPanelAnswer);
|
||||
customElements.define('ai-panel-input', AIPanelInput);
|
||||
customElements.define('ai-panel-generating', AIPanelGenerating);
|
||||
customElements.define('ai-panel-error', AIPanelError);
|
||||
customElements.define('chat-assistant-avatar', AssistantAvatar);
|
||||
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);
|
||||
customElements.define('ai-block-diff-options', BlockDiffOptions);
|
||||
customElements.define(AFFINE_BLOCK_DIFF_PLAYGROUND, BlockDiffPlayground);
|
||||
customElements.define(
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL,
|
||||
BlockDiffPlaygroundModal
|
||||
);
|
||||
|
||||
customElements.define('tool-call-card', ToolCallCard);
|
||||
customElements.define('tool-result-card', ToolResultCard);
|
||||
customElements.define('tool-call-failed', ToolFailedCard);
|
||||
customElements.define('doc-semantic-search-result', DocSemanticSearchResult);
|
||||
customElements.define('doc-keyword-search-result', DocKeywordSearchResult);
|
||||
customElements.define('doc-read-result', DocReadResult);
|
||||
customElements.define('doc-write-tool', DocWriteTool);
|
||||
customElements.define('web-crawl-tool', WebCrawlTool);
|
||||
customElements.define('web-search-tool', WebSearchTool);
|
||||
customElements.define('section-edit-tool', SectionEditTool);
|
||||
customElements.define('doc-compose-tool', DocComposeTool);
|
||||
customElements.define('code-artifact-tool', CodeArtifactTool);
|
||||
customElements.define('code-highlighter', CodeHighlighter);
|
||||
customElements.define('artifact-preview-panel', ArtifactPreviewPanel);
|
||||
customElements.define('doc-edit-tool', DocEditTool);
|
||||
|
||||
customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget);
|
||||
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);
|
||||
customElements.define(
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
|
||||
AffineBlockDiffWidgetForBlock
|
||||
);
|
||||
customElements.define(
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
|
||||
AffineBlockDiffWidgetForPage
|
||||
);
|
||||
|
||||
customElements.define('edgeless-copilot-panel', EdgelessCopilotPanel);
|
||||
customElements.define(
|
||||
'edgeless-copilot-toolbar-entry',
|
||||
EdgelessCopilotToolbarEntry
|
||||
);
|
||||
|
||||
customElements.define('transcription-block', LitTranscriptionBlock);
|
||||
customElements.define('chat-panel-split-view', ChatPanelSplitView);
|
||||
customElements.define('artifact-skeleton', ArtifactSkeleton);
|
||||
}
|
||||
95
packages/frontend/core/src/blocksuite/ai/effects/app.ts
Normal file
95
packages/frontend/core/src/blocksuite/ai/effects/app.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ActionWrapper } from '../chat-panel/actions/action-wrapper';
|
||||
import { ActionImage } from '../chat-panel/actions/image';
|
||||
import { ActionImageToText } from '../chat-panel/actions/image-to-text';
|
||||
import { ActionMakeReal } from '../chat-panel/actions/make-real';
|
||||
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 { ChatMessageAction } from '../chat-panel/message/action';
|
||||
import { ChatMessageAssistant } from '../chat-panel/message/assistant';
|
||||
import { ChatMessageUser } from '../chat-panel/message/user';
|
||||
import { AIChatAddContext } from '../components/ai-chat-add-context';
|
||||
import { ChatPanelAddPopover } from '../components/ai-chat-chips/add-popover';
|
||||
import { ChatPanelAttachmentChip } from '../components/ai-chat-chips/attachment-chip';
|
||||
import { ChatPanelCandidatesPopover } from '../components/ai-chat-chips/candidates-popover';
|
||||
import { ChatPanelChips } from '../components/ai-chat-chips/chat-panel-chips';
|
||||
import { ChatPanelChip } from '../components/ai-chat-chips/chip';
|
||||
import { ChatPanelCollectionChip } from '../components/ai-chat-chips/collection-chip';
|
||||
import { ChatPanelDocChip } from '../components/ai-chat-chips/doc-chip';
|
||||
import { ChatPanelFileChip } from '../components/ai-chat-chips/file-chip';
|
||||
import { ChatPanelSelectedChip } from '../components/ai-chat-chips/selected-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 { ChatPanelSplitView } from '../components/ai-chat-content/split-view';
|
||||
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 { AIChatToolbar, AISessionHistory } from '../components/ai-chat-toolbar';
|
||||
import { AIHistoryClear } from '../components/ai-history-clear';
|
||||
import { AssistantAvatar } from '../components/ai-message-content/assistant-avatar';
|
||||
import { ChatActionList } from '../components/chat-action-list';
|
||||
import { ChatCopyMore } from '../components/copy-more';
|
||||
import { ImagePreviewGrid } from '../components/image-preview-grid';
|
||||
import { effects as componentPlaygroundEffects } from '../components/playground';
|
||||
import { AIChatBlockPeekView } from '../peek-view/chat-block-peek-view';
|
||||
import { DateTime } from '../peek-view/date-time';
|
||||
import { type AppEffectElementTag, appEffectElementTags } from './registry';
|
||||
import { registerAISharedEffects } from './shared';
|
||||
|
||||
const appRegistries = new WeakSet<CustomElementRegistry>();
|
||||
const appElements = {
|
||||
'chat-action-list': ChatActionList,
|
||||
'chat-copy-more': ChatCopyMore,
|
||||
'image-preview-grid': ImagePreviewGrid,
|
||||
'action-wrapper': ActionWrapper,
|
||||
'action-image-to-text': ActionImageToText,
|
||||
'action-image': ActionImage,
|
||||
'action-make-real': ActionMakeReal,
|
||||
'action-mindmap': ActionMindmap,
|
||||
'action-slides': ActionSlides,
|
||||
'action-text': ActionText,
|
||||
'ai-loading': AILoading,
|
||||
'ai-chat-content': AIChatContent,
|
||||
'ai-chat-toolbar': AIChatToolbar,
|
||||
'ai-session-history': AISessionHistory,
|
||||
'ai-chat-messages': AIChatMessages,
|
||||
'ai-chat-input': AIChatInput,
|
||||
'ai-chat-add-context': AIChatAddContext,
|
||||
'ai-chat-embedding-status-tooltip': AIChatEmbeddingStatusTooltip,
|
||||
'ai-chat-composer': AIChatComposer,
|
||||
'chat-panel-chips': ChatPanelChips,
|
||||
'ai-history-clear': AIHistoryClear,
|
||||
'chat-panel-add-popover': ChatPanelAddPopover,
|
||||
'chat-input-preference': ChatInputPreference,
|
||||
'chat-panel-candidates-popover': ChatPanelCandidatesPopover,
|
||||
'chat-panel-doc-chip': ChatPanelDocChip,
|
||||
'chat-panel-file-chip': ChatPanelFileChip,
|
||||
'chat-panel-tag-chip': ChatPanelTagChip,
|
||||
'chat-panel-collection-chip': ChatPanelCollectionChip,
|
||||
'chat-panel-selected-chip': ChatPanelSelectedChip,
|
||||
'chat-panel-attachment-chip': ChatPanelAttachmentChip,
|
||||
'chat-panel-chip': ChatPanelChip,
|
||||
'chat-assistant-avatar': AssistantAvatar,
|
||||
'chat-message-action': ChatMessageAction,
|
||||
'chat-message-assistant': ChatMessageAssistant,
|
||||
'chat-message-user': ChatMessageUser,
|
||||
'ai-chat-block-peek-view': AIChatBlockPeekView,
|
||||
'date-time': DateTime,
|
||||
'chat-panel-split-view': ChatPanelSplitView,
|
||||
} satisfies Record<AppEffectElementTag, CustomElementConstructor>;
|
||||
|
||||
export function registerAIAppEffects() {
|
||||
const registry = customElements;
|
||||
if (appRegistries.has(registry)) return;
|
||||
appRegistries.add(registry);
|
||||
|
||||
registerAISharedEffects();
|
||||
componentPlaygroundEffects();
|
||||
|
||||
for (const tag of appEffectElementTags) {
|
||||
customElements.define(tag, appElements[tag]);
|
||||
}
|
||||
}
|
||||
105
packages/frontend/core/src/blocksuite/ai/effects/editor.ts
Normal file
105
packages/frontend/core/src/blocksuite/ai/effects/editor.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
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 {
|
||||
AIChatBlockMessage,
|
||||
AIChatBlockMessages,
|
||||
} from '../blocks/ai-chat-block/components/ai-chat-messages';
|
||||
import {
|
||||
ChatImage,
|
||||
ChatImages,
|
||||
} from '../blocks/ai-chat-block/components/chat-images';
|
||||
import { ImagePlaceholder } from '../blocks/ai-chat-block/components/image-placeholder';
|
||||
import { UserInfo } from '../blocks/ai-chat-block/components/user-info';
|
||||
import { effects as componentAiItemEffects } from '../components/ai-item';
|
||||
import { AIScrollableTextRenderer } from '../components/ai-scrollable-text-renderer';
|
||||
import { AskAIButton } from '../components/ask-ai-button';
|
||||
import { AskAIIcon } from '../components/ask-ai-icon';
|
||||
import { AskAIPanel } from '../components/ask-ai-panel';
|
||||
import { AskAIToolbarButton } from '../components/ask-ai-toolbar';
|
||||
import {
|
||||
AFFINE_AI_PANEL_WIDGET,
|
||||
AffineAIPanelWidget,
|
||||
} from '../widgets/ai-panel/ai-panel';
|
||||
import {
|
||||
AIPanelAnswer,
|
||||
AIPanelDivider,
|
||||
AIPanelError,
|
||||
AIPanelGenerating,
|
||||
AIPanelInput,
|
||||
} from '../widgets/ai-panel/components';
|
||||
import { AIFinishTip } from '../widgets/ai-panel/components/finish-tip';
|
||||
import { GeneratingPlaceholder } from '../widgets/ai-panel/components/generating-placeholder';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
|
||||
AffineBlockDiffWidgetForBlock,
|
||||
} from '../widgets/block-diff/block';
|
||||
import { BlockDiffOptions } from '../widgets/block-diff/options';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
|
||||
AffineBlockDiffWidgetForPage,
|
||||
} from '../widgets/block-diff/page';
|
||||
import {
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND,
|
||||
AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL,
|
||||
BlockDiffPlayground,
|
||||
BlockDiffPlaygroundModal,
|
||||
} from '../widgets/block-diff/playground';
|
||||
import {
|
||||
AFFINE_EDGELESS_COPILOT_WIDGET,
|
||||
EdgelessCopilotWidget,
|
||||
} from '../widgets/edgeless-copilot';
|
||||
import { EdgelessCopilotPanel } from '../widgets/edgeless-copilot-panel';
|
||||
import { EdgelessCopilotToolbarEntry } from '../widgets/edgeless-copilot-panel/toolbar-entry';
|
||||
import {
|
||||
type EditorEffectElementTag,
|
||||
editorEffectElementTags,
|
||||
} from './registry';
|
||||
import { registerAISharedEffects } from './shared';
|
||||
|
||||
const editorRegistries = new WeakSet<CustomElementRegistry>();
|
||||
const editorElements = {
|
||||
'ask-ai-icon': AskAIIcon,
|
||||
'ask-ai-button': AskAIButton,
|
||||
'ask-ai-toolbar-button': AskAIToolbarButton,
|
||||
'ask-ai-panel': AskAIPanel,
|
||||
'affine-edgeless-ai-chat': EdgelessAIChatBlockComponent,
|
||||
'affine-ai-chat': AIChatBlockComponent,
|
||||
'ai-chat-block-message': AIChatBlockMessage,
|
||||
'ai-chat-block-messages': AIChatBlockMessages,
|
||||
'ai-scrollable-text-renderer': AIScrollableTextRenderer,
|
||||
'image-placeholder': ImagePlaceholder,
|
||||
'chat-image': ChatImage,
|
||||
'chat-images': ChatImages,
|
||||
'user-info': UserInfo,
|
||||
'generating-placeholder': GeneratingPlaceholder,
|
||||
'ai-finish-tip': AIFinishTip,
|
||||
'ai-panel-divider': AIPanelDivider,
|
||||
'ai-panel-answer': AIPanelAnswer,
|
||||
'ai-panel-input': AIPanelInput,
|
||||
'ai-panel-generating': AIPanelGenerating,
|
||||
'ai-panel-error': AIPanelError,
|
||||
'ai-block-diff-options': BlockDiffOptions,
|
||||
[AFFINE_BLOCK_DIFF_PLAYGROUND]: BlockDiffPlayground,
|
||||
[AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL]: BlockDiffPlaygroundModal,
|
||||
[AFFINE_AI_PANEL_WIDGET]: AffineAIPanelWidget,
|
||||
[AFFINE_EDGELESS_COPILOT_WIDGET]: EdgelessCopilotWidget,
|
||||
[AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK]: AffineBlockDiffWidgetForBlock,
|
||||
[AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE]: AffineBlockDiffWidgetForPage,
|
||||
'edgeless-copilot-panel': EdgelessCopilotPanel,
|
||||
'edgeless-copilot-toolbar-entry': EdgelessCopilotToolbarEntry,
|
||||
'transcription-block': LitTranscriptionBlock,
|
||||
} satisfies Record<EditorEffectElementTag, CustomElementConstructor>;
|
||||
|
||||
export function registerAIEditorEffects() {
|
||||
const registry = customElements;
|
||||
if (editorRegistries.has(registry)) return;
|
||||
editorRegistries.add(registry);
|
||||
|
||||
registerAISharedEffects();
|
||||
componentAiItemEffects();
|
||||
|
||||
for (const tag of editorEffectElementTags) {
|
||||
customElements.define(tag, editorElements[tag]);
|
||||
}
|
||||
}
|
||||
106
packages/frontend/core/src/blocksuite/ai/effects/registry.ts
Normal file
106
packages/frontend/core/src/blocksuite/ai/effects/registry.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
export const sharedEffectElementTags = [
|
||||
'ai-error-wrapper',
|
||||
'ai-slides-renderer',
|
||||
'ai-answer-wrapper',
|
||||
'chat-content-images',
|
||||
'chat-content-pure-text',
|
||||
'chat-content-rich-text',
|
||||
'chat-content-stream-objects',
|
||||
'text-renderer',
|
||||
'tool-call-card',
|
||||
'tool-result-card',
|
||||
'tool-call-failed',
|
||||
'doc-semantic-search-result',
|
||||
'doc-keyword-search-result',
|
||||
'doc-read-result',
|
||||
'doc-write-tool',
|
||||
'web-crawl-tool',
|
||||
'web-search-tool',
|
||||
'section-edit-tool',
|
||||
'doc-compose-tool',
|
||||
'code-artifact-tool',
|
||||
'code-highlighter',
|
||||
'artifact-preview-panel',
|
||||
'doc-edit-tool',
|
||||
'artifact-skeleton',
|
||||
] as const;
|
||||
|
||||
export type SharedEffectElementTag = (typeof sharedEffectElementTags)[number];
|
||||
|
||||
export const editorEffectElementTags = [
|
||||
'ask-ai-icon',
|
||||
'ask-ai-button',
|
||||
'ask-ai-toolbar-button',
|
||||
'ask-ai-panel',
|
||||
'affine-edgeless-ai-chat',
|
||||
'affine-ai-chat',
|
||||
'ai-chat-block-message',
|
||||
'ai-chat-block-messages',
|
||||
'ai-scrollable-text-renderer',
|
||||
'image-placeholder',
|
||||
'chat-image',
|
||||
'chat-images',
|
||||
'user-info',
|
||||
'generating-placeholder',
|
||||
'ai-finish-tip',
|
||||
'ai-panel-divider',
|
||||
'ai-panel-answer',
|
||||
'ai-panel-input',
|
||||
'ai-panel-generating',
|
||||
'ai-panel-error',
|
||||
'ai-block-diff-options',
|
||||
'affine-block-diff-playground',
|
||||
'affine-block-diff-playground-modal',
|
||||
'affine-ai-panel-widget',
|
||||
'affine-edgeless-copilot-widget',
|
||||
'affine-block-diff-widget-for-block',
|
||||
'affine-block-diff-widget-for-page',
|
||||
'edgeless-copilot-panel',
|
||||
'edgeless-copilot-toolbar-entry',
|
||||
'transcription-block',
|
||||
] as const;
|
||||
|
||||
export type EditorEffectElementTag = (typeof editorEffectElementTags)[number];
|
||||
|
||||
export const appEffectElementTags = [
|
||||
'chat-action-list',
|
||||
'chat-copy-more',
|
||||
'image-preview-grid',
|
||||
'action-wrapper',
|
||||
'action-image-to-text',
|
||||
'action-image',
|
||||
'action-make-real',
|
||||
'action-mindmap',
|
||||
'action-slides',
|
||||
'action-text',
|
||||
'ai-loading',
|
||||
'ai-chat-content',
|
||||
'ai-chat-toolbar',
|
||||
'ai-session-history',
|
||||
'ai-chat-messages',
|
||||
'ai-chat-input',
|
||||
'ai-chat-add-context',
|
||||
'ai-chat-embedding-status-tooltip',
|
||||
'ai-chat-composer',
|
||||
'chat-panel-chips',
|
||||
'ai-history-clear',
|
||||
'chat-panel-add-popover',
|
||||
'chat-input-preference',
|
||||
'chat-panel-candidates-popover',
|
||||
'chat-panel-doc-chip',
|
||||
'chat-panel-file-chip',
|
||||
'chat-panel-tag-chip',
|
||||
'chat-panel-collection-chip',
|
||||
'chat-panel-selected-chip',
|
||||
'chat-panel-attachment-chip',
|
||||
'chat-panel-chip',
|
||||
'chat-assistant-avatar',
|
||||
'chat-message-action',
|
||||
'chat-message-assistant',
|
||||
'chat-message-user',
|
||||
'ai-chat-block-peek-view',
|
||||
'date-time',
|
||||
'chat-panel-split-view',
|
||||
] as const;
|
||||
|
||||
export type AppEffectElementTag = (typeof appEffectElementTags)[number];
|
||||
74
packages/frontend/core/src/blocksuite/ai/effects/shared.ts
Normal file
74
packages/frontend/core/src/blocksuite/ai/effects/shared.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { effects as tooltipEffects } from '@blocksuite/affine-components/tooltip';
|
||||
|
||||
import { ArtifactSkeleton } from '../components/ai-artifact-skeleton';
|
||||
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 { ArtifactPreviewPanel } from '../components/ai-tools/artifacts-preview-panel';
|
||||
import {
|
||||
CodeArtifactTool,
|
||||
CodeHighlighter,
|
||||
} from '../components/ai-tools/code-artifact';
|
||||
import { DocComposeTool } from '../components/ai-tools/doc-compose';
|
||||
import { DocEditTool } from '../components/ai-tools/doc-edit';
|
||||
import { DocKeywordSearchResult } from '../components/ai-tools/doc-keyword-search-result';
|
||||
import { DocReadResult } from '../components/ai-tools/doc-read-result';
|
||||
import { DocSemanticSearchResult } from '../components/ai-tools/doc-semantic-search-result';
|
||||
import { DocWriteTool } from '../components/ai-tools/doc-write';
|
||||
import { SectionEditTool } from '../components/ai-tools/section-edit';
|
||||
import { ToolCallCard } from '../components/ai-tools/tool-call-card';
|
||||
import { ToolFailedCard } from '../components/ai-tools/tool-failed-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 { TextRenderer } from '../components/text-renderer';
|
||||
import { AIErrorWrapper } from '../messages/error';
|
||||
import { AISlidesRenderer } from '../messages/slides-renderer';
|
||||
import { AIAnswerWrapper } from '../messages/wrapper';
|
||||
import { registerMiniMindmapBlocks } from '../mini-mindmap';
|
||||
import {
|
||||
type SharedEffectElementTag,
|
||||
sharedEffectElementTags,
|
||||
} from './registry';
|
||||
|
||||
const sharedRegistries = new WeakSet<CustomElementRegistry>();
|
||||
const sharedElements = {
|
||||
'ai-error-wrapper': AIErrorWrapper,
|
||||
'ai-slides-renderer': AISlidesRenderer,
|
||||
'ai-answer-wrapper': AIAnswerWrapper,
|
||||
'chat-content-images': ChatContentImages,
|
||||
'chat-content-pure-text': ChatContentPureText,
|
||||
'chat-content-rich-text': ChatContentRichText,
|
||||
'chat-content-stream-objects': ChatContentStreamObjects,
|
||||
'text-renderer': TextRenderer,
|
||||
'tool-call-card': ToolCallCard,
|
||||
'tool-result-card': ToolResultCard,
|
||||
'tool-call-failed': ToolFailedCard,
|
||||
'doc-semantic-search-result': DocSemanticSearchResult,
|
||||
'doc-keyword-search-result': DocKeywordSearchResult,
|
||||
'doc-read-result': DocReadResult,
|
||||
'doc-write-tool': DocWriteTool,
|
||||
'web-crawl-tool': WebCrawlTool,
|
||||
'web-search-tool': WebSearchTool,
|
||||
'section-edit-tool': SectionEditTool,
|
||||
'doc-compose-tool': DocComposeTool,
|
||||
'code-artifact-tool': CodeArtifactTool,
|
||||
'code-highlighter': CodeHighlighter,
|
||||
'artifact-preview-panel': ArtifactPreviewPanel,
|
||||
'doc-edit-tool': DocEditTool,
|
||||
'artifact-skeleton': ArtifactSkeleton,
|
||||
} satisfies Record<SharedEffectElementTag, CustomElementConstructor>;
|
||||
|
||||
export function registerAISharedEffects() {
|
||||
const registry = customElements;
|
||||
if (sharedRegistries.has(registry)) return;
|
||||
sharedRegistries.add(registry);
|
||||
|
||||
registerMiniMindmapBlocks();
|
||||
tooltipEffects();
|
||||
|
||||
for (const tag of sharedEffectElementTags) {
|
||||
customElements.define(tag, sharedElements[tag]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './_common/config';
|
||||
export * from './actions';
|
||||
export { ChatPanel } from './chat-panel';
|
||||
export * from './entries';
|
||||
export * from './entries/edgeless/actions-config';
|
||||
export * from './messages';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { registerAIEffects } from '@affine/core/blocksuite/ai/effects';
|
||||
import { registerAIEditorEffects } from '@affine/core/blocksuite/ai/effects/editor';
|
||||
import { editorEffects } from '@affine/core/blocksuite/editors';
|
||||
|
||||
import { registerTemplates } from './register-templates';
|
||||
|
||||
editorEffects();
|
||||
registerAIEffects();
|
||||
registerAIEditorEffects();
|
||||
registerTemplates();
|
||||
|
||||
export * from './blocksuite-editor';
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
EventSourceService,
|
||||
FetchService,
|
||||
GraphQLService,
|
||||
ServerService,
|
||||
SubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
@@ -228,6 +229,7 @@ export const Component = () => {
|
||||
);
|
||||
content.aiDraftService = framework.get(AIDraftService);
|
||||
content.aiToolsConfigService = framework.get(AIToolsConfigService);
|
||||
content.serverService = framework.get(ServerService);
|
||||
content.subscriptionService = framework.get(SubscriptionService);
|
||||
content.aiModelService = framework.get(AIModelService);
|
||||
content.onAISubscribe = handleAISubscribe;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
import { PageDetailLoading } from '@affine/component/page-detail-skeleton';
|
||||
import type { AIChatParams, ChatPanel } from '@affine/core/blocksuite/ai';
|
||||
import type { AIChatParams } 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';
|
||||
@@ -103,7 +103,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
|
||||
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||
|
||||
const peekView = useService(PeekViewService).peekView;
|
||||
|
||||
@@ -373,7 +372,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
icon={<AiIcon />}
|
||||
unmountOnInactive={false}
|
||||
>
|
||||
<EditorChatPanel editor={editorContainer} ref={chatPanelRef} />
|
||||
<EditorChatPanel editor={editorContainer} />
|
||||
</ViewSidebarTab>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/* eslint-disable rxjs/finnish */
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveInitialSession,
|
||||
type SessionService,
|
||||
type WorkbenchLike,
|
||||
} from './chat-panel-session';
|
||||
|
||||
const createWorkbench = (search: string) => {
|
||||
const updateQueryString = vi.fn();
|
||||
const workbench = {
|
||||
location$: { value: { search } },
|
||||
activeView$: { value: { updateQueryString } },
|
||||
} satisfies WorkbenchLike;
|
||||
|
||||
return { workbench, updateQueryString };
|
||||
};
|
||||
|
||||
const doc = { id: 'doc-1', workspace: { id: 'ws-1' } };
|
||||
|
||||
test('returns undefined without session service or doc', async () => {
|
||||
await expect(
|
||||
resolveInitialSession({ sessionService: null, doc, workbench: null })
|
||||
).resolves.toBeUndefined();
|
||||
await expect(
|
||||
resolveInitialSession({
|
||||
sessionService: {
|
||||
getSessions: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
},
|
||||
doc: null,
|
||||
workbench: null,
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
describe('resolveInitialSession', () => {
|
||||
test('prefers pinned session and clears sessionId from url', async () => {
|
||||
const pinnedSession = {
|
||||
sessionId: 'pinned-session',
|
||||
pinned: true,
|
||||
} as CopilotChatHistoryFragment;
|
||||
|
||||
const sessionService: SessionService = {
|
||||
getSessions: vi.fn().mockResolvedValueOnce([pinnedSession]),
|
||||
getSession: vi.fn(),
|
||||
};
|
||||
|
||||
const { workbench, updateQueryString } = createWorkbench(
|
||||
'?sessionId=from-url'
|
||||
);
|
||||
|
||||
const result = await resolveInitialSession({
|
||||
sessionService,
|
||||
doc,
|
||||
workbench,
|
||||
});
|
||||
|
||||
expect(result).toBe(pinnedSession);
|
||||
expect(updateQueryString).toHaveBeenCalledWith(
|
||||
{ sessionId: undefined },
|
||||
{ replace: true }
|
||||
);
|
||||
expect(sessionService.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('loads session from url when no pinned session', async () => {
|
||||
const sessionFromUrl = {
|
||||
sessionId: 'url-session',
|
||||
pinned: false,
|
||||
} as CopilotChatHistoryFragment;
|
||||
|
||||
const sessionService: SessionService = {
|
||||
getSessions: vi.fn().mockResolvedValueOnce([]),
|
||||
getSession: vi.fn().mockResolvedValueOnce(sessionFromUrl),
|
||||
};
|
||||
|
||||
const { workbench, updateQueryString } = createWorkbench(
|
||||
'?sessionId=url-session'
|
||||
);
|
||||
|
||||
const result = await resolveInitialSession({
|
||||
sessionService,
|
||||
doc,
|
||||
workbench,
|
||||
});
|
||||
|
||||
expect(result).toBe(sessionFromUrl);
|
||||
expect(sessionService.getSession).toHaveBeenCalledWith(
|
||||
doc.workspace.id,
|
||||
'url-session'
|
||||
);
|
||||
expect(updateQueryString).toHaveBeenCalledWith(
|
||||
{ sessionId: undefined },
|
||||
{ replace: true }
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to latest doc session', async () => {
|
||||
const docSession = {
|
||||
sessionId: 'doc-session',
|
||||
pinned: false,
|
||||
} as CopilotChatHistoryFragment;
|
||||
|
||||
const sessionService: SessionService = {
|
||||
getSessions: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([docSession]),
|
||||
getSession: vi.fn(),
|
||||
};
|
||||
|
||||
const { workbench } = createWorkbench('');
|
||||
|
||||
const result = await resolveInitialSession({
|
||||
sessionService,
|
||||
doc,
|
||||
workbench,
|
||||
});
|
||||
|
||||
expect(result).toBe(docSession);
|
||||
expect(sessionService.getSessions).toHaveBeenCalledWith(
|
||||
doc.workspace.id,
|
||||
doc.id,
|
||||
{ action: false, fork: false, limit: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null when url session is missing', async () => {
|
||||
const sessionService: SessionService = {
|
||||
getSessions: vi.fn().mockResolvedValueOnce([]),
|
||||
getSession: vi.fn().mockResolvedValueOnce(null),
|
||||
};
|
||||
|
||||
const { workbench } = createWorkbench('?sessionId=missing');
|
||||
|
||||
const result = await resolveInitialSession({
|
||||
sessionService,
|
||||
doc,
|
||||
workbench,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/* eslint-disable rxjs/finnish */
|
||||
import type { CopilotChatHistoryFragment } from '@affine/graphql';
|
||||
|
||||
type SessionListOptions = {
|
||||
pinned?: boolean;
|
||||
action?: boolean;
|
||||
fork?: boolean;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export interface SessionService {
|
||||
getSessions: (
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: SessionListOptions
|
||||
) => Promise<CopilotChatHistoryFragment[] | null | undefined>;
|
||||
getSession: (
|
||||
workspaceId: string,
|
||||
sessionId: string
|
||||
) => Promise<CopilotChatHistoryFragment | null | undefined>;
|
||||
}
|
||||
|
||||
export interface WorkbenchLike {
|
||||
location$: {
|
||||
value: {
|
||||
search: string;
|
||||
};
|
||||
};
|
||||
activeView$: {
|
||||
value: {
|
||||
updateQueryString: (
|
||||
patch: Record<string, unknown>,
|
||||
options?: { replace?: boolean }
|
||||
) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DocLike {
|
||||
id: string;
|
||||
workspace: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const getSessionIdFromUrl = (workbench?: WorkbenchLike | null) => {
|
||||
if (!workbench) {
|
||||
return undefined;
|
||||
}
|
||||
const searchParams = new URLSearchParams(workbench.location$.value.search);
|
||||
const sessionId = searchParams.get('sessionId');
|
||||
if (sessionId) {
|
||||
workbench.activeView$.value.updateQueryString(
|
||||
{ sessionId: undefined },
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
return sessionId ?? undefined;
|
||||
};
|
||||
|
||||
export const resolveInitialSession = async ({
|
||||
sessionService,
|
||||
doc,
|
||||
workbench,
|
||||
}: {
|
||||
sessionService?: SessionService | null;
|
||||
doc?: DocLike | null;
|
||||
workbench?: WorkbenchLike | null;
|
||||
}): Promise<CopilotChatHistoryFragment | null | undefined> => {
|
||||
if (!sessionService || !doc) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sessionId = getSessionIdFromUrl(workbench);
|
||||
|
||||
const pinSessions = await sessionService.getSessions(
|
||||
doc.workspace.id,
|
||||
undefined,
|
||||
{
|
||||
pinned: true,
|
||||
limit: 1,
|
||||
}
|
||||
);
|
||||
|
||||
if (Array.isArray(pinSessions) && pinSessions[0]) {
|
||||
return pinSessions[0];
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
const session = await sessionService.getSession(
|
||||
doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
return session ?? null;
|
||||
}
|
||||
|
||||
const docSessions = await sessionService.getSessions(
|
||||
doc.workspace.id,
|
||||
doc.id,
|
||||
{
|
||||
action: false,
|
||||
fork: false,
|
||||
limit: 1,
|
||||
}
|
||||
);
|
||||
|
||||
return docSessions?.[0] ?? null;
|
||||
};
|
||||
@@ -1,6 +1,100 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
userSelect: 'text',
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const header = style({
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
position: 'relative',
|
||||
padding: '8px var(--h-padding, 16px)',
|
||||
width: '100%',
|
||||
height: '36px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const playground = style({
|
||||
cursor: 'pointer',
|
||||
padding: '2px',
|
||||
marginLeft: '8px',
|
||||
marginRight: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
flexGrow: 1,
|
||||
height: 0,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const loadingContainer = style({
|
||||
position: 'relative',
|
||||
padding: '44px 0 166px 0',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const loading = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const loadingTitle = style({
|
||||
fontWeight: 600,
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
|
||||
export const loadingIcon = style({
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
color: 'var(--affine-icon-secondary)',
|
||||
});
|
||||
|
||||
globalStyle(`${playground} svg`, {
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
|
||||
globalStyle(`${playground}:hover svg`, {
|
||||
color: cssVarV2('icon/activated'),
|
||||
});
|
||||
|
||||
globalStyle(`${content} > ai-chat-content`, {
|
||||
flexGrow: 1,
|
||||
height: 0,
|
||||
minHeight: 0,
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useConfirmModal } from '@affine/component';
|
||||
import { AIProvider, ChatPanel } from '@affine/core/blocksuite/ai';
|
||||
import { AIProvider } from '@affine/core/blocksuite/ai';
|
||||
import type { AppSidebarConfig } from '@affine/core/blocksuite/ai/chat-panel/chat-config';
|
||||
import {
|
||||
AIChatContent,
|
||||
type ChatContextValue,
|
||||
} from '@affine/core/blocksuite/ai/components/ai-chat-content';
|
||||
import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages';
|
||||
import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
||||
import { createPlaygroundModal } from '@affine/core/blocksuite/ai/components/playground/modal';
|
||||
import { registerAIAppEffects } from '@affine/core/blocksuite/ai/effects/app';
|
||||
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
|
||||
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
|
||||
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
||||
@@ -12,48 +21,49 @@ import {
|
||||
import { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||
import { ServerService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useSignalValue } from '@affine/core/modules/doc-info/utils';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type {
|
||||
ContextEmbedStatus,
|
||||
CopilotChatHistoryFragment,
|
||||
UpdateChatSessionInput,
|
||||
} from '@affine/graphql';
|
||||
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
|
||||
import { DocModeProvider } from '@blocksuite/affine/shared/services';
|
||||
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
||||
import { CenterPeekIcon, Logo1Icon } from '@blocksuite/icons/rc';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
import { useFramework, useService } from '@toeverything/infra';
|
||||
import { forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import { html } from 'lit';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import * as styles from './chat.css';
|
||||
import {
|
||||
resolveInitialSession,
|
||||
type WorkbenchLike,
|
||||
} from './chat-panel-session';
|
||||
|
||||
registerAIAppEffects();
|
||||
|
||||
export interface SidebarTabProps {
|
||||
editor: AffineEditorContainer | null;
|
||||
onLoad?: ((component: HTMLElement) => void) | null;
|
||||
}
|
||||
|
||||
// A wrapper for CopilotPanel
|
||||
export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
{ editor, onLoad }: SidebarTabProps,
|
||||
ref: React.ForwardedRef<ChatPanel>
|
||||
) {
|
||||
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
||||
const framework = useFramework();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
useEffect(() => {
|
||||
if (onLoad && chatPanelRef.current) {
|
||||
(chatPanelRef.current as ChatPanel).updateComplete
|
||||
.then(() => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(chatPanelRef.current);
|
||||
} else {
|
||||
ref.current = chatPanelRef.current;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}, [onLoad, ref]);
|
||||
const { closeConfirmModal, openConfirmModal } = useConfirmModal();
|
||||
const notificationService = useMemo(
|
||||
() => new NotificationServiceImpl(closeConfirmModal, openConfirmModal),
|
||||
[closeConfirmModal, openConfirmModal]
|
||||
);
|
||||
const specs = useAISpecs();
|
||||
const handleAISubscribe = useAISubscribe();
|
||||
|
||||
const {
|
||||
docDisplayConfig,
|
||||
@@ -61,101 +71,477 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
reasoningConfig,
|
||||
playgroundConfig,
|
||||
} = useAIChatConfig();
|
||||
const confirmModal = useConfirmModal();
|
||||
const specs = useAISpecs();
|
||||
const handleAISubscribe = useAISubscribe();
|
||||
const playgroundVisible = useSignalValue(playgroundConfig.visible) ?? false;
|
||||
|
||||
const [session, setSession] = useState<
|
||||
CopilotChatHistoryFragment | null | undefined
|
||||
>(undefined);
|
||||
const [embeddingProgress, setEmbeddingProgress] = useState<[number, number]>([
|
||||
0, 0,
|
||||
]);
|
||||
const [status, setStatus] = useState<ChatStatus>('idle');
|
||||
const [hasPinned, setHasPinned] = useState(false);
|
||||
|
||||
const [chatContent, setChatContent] = useState<AIChatContent | null>(null);
|
||||
const [chatToolbar, setChatToolbar] = useState<AIChatToolbar | null>(null);
|
||||
const [isBodyProvided, setIsBodyProvided] = useState(false);
|
||||
const [isHeaderProvided, setIsHeaderProvided] = useState(false);
|
||||
|
||||
const chatContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const chatToolbarContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const contentKeyRef = useRef<string | null>(null);
|
||||
const lastDocIdRef = useRef<string | null>(null);
|
||||
|
||||
const doc = editor?.doc;
|
||||
const host = editor?.host;
|
||||
|
||||
const appSidebarConfig = useMemo<AppSidebarConfig>(() => {
|
||||
return {
|
||||
getWidth: () =>
|
||||
createSignalFromObservable<number | undefined>(
|
||||
workbench.sidebarWidth$.asObservable(),
|
||||
0
|
||||
),
|
||||
isOpen: () =>
|
||||
createSignalFromObservable<boolean | undefined>(
|
||||
workbench.sidebarOpen$.asObservable(),
|
||||
true
|
||||
),
|
||||
};
|
||||
}, [workbench]);
|
||||
|
||||
const [sidebarWidthSignal, setSidebarWidthSignal] =
|
||||
useState<Signal<number | undefined>>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !editor.host) return;
|
||||
const { signal, cleanup } = appSidebarConfig.getWidth();
|
||||
setSidebarWidthSignal(signal);
|
||||
return cleanup;
|
||||
}, [appSidebarConfig]);
|
||||
|
||||
if (!chatPanelRef.current) {
|
||||
chatPanelRef.current = new ChatPanel();
|
||||
chatPanelRef.current.host = editor.host;
|
||||
chatPanelRef.current.doc = editor.doc;
|
||||
const resetPanel = useCallback(() => {
|
||||
setSession(undefined);
|
||||
setEmbeddingProgress([0, 0]);
|
||||
setHasPinned(false);
|
||||
}, []);
|
||||
|
||||
const workbench = framework.get(WorkbenchService).workbench;
|
||||
chatPanelRef.current.appSidebarConfig = {
|
||||
getWidth: () => {
|
||||
const width$ = workbench.sidebarWidth$;
|
||||
return createSignalFromObservable(width$, 0);
|
||||
},
|
||||
isOpen: () => {
|
||||
const open$ = workbench.sidebarOpen$;
|
||||
return createSignalFromObservable(open$, true);
|
||||
},
|
||||
};
|
||||
const initPanel = useCallback(async () => {
|
||||
try {
|
||||
const nextSession = await resolveInitialSession({
|
||||
sessionService: AIProvider.session ?? undefined,
|
||||
doc,
|
||||
workbench: workbench as WorkbenchLike,
|
||||
});
|
||||
|
||||
chatPanelRef.current.docDisplayConfig = docDisplayConfig;
|
||||
chatPanelRef.current.searchMenuConfig = searchMenuConfig;
|
||||
chatPanelRef.current.reasoningConfig = reasoningConfig;
|
||||
chatPanelRef.current.playgroundConfig = playgroundConfig;
|
||||
chatPanelRef.current.extensions = specs;
|
||||
chatPanelRef.current.serverService = framework.get(ServerService);
|
||||
chatPanelRef.current.affineFeatureFlagService =
|
||||
framework.get(FeatureFlagService);
|
||||
chatPanelRef.current.affineWorkspaceDialogService = framework.get(
|
||||
WorkspaceDialogService
|
||||
);
|
||||
chatPanelRef.current.affineWorkbenchService =
|
||||
framework.get(WorkbenchService);
|
||||
chatPanelRef.current.affineThemeService = framework.get(AppThemeService);
|
||||
chatPanelRef.current.peekViewService = framework.get(PeekViewService);
|
||||
chatPanelRef.current.notificationService = new NotificationServiceImpl(
|
||||
confirmModal.closeConfirmModal,
|
||||
confirmModal.openConfirmModal
|
||||
);
|
||||
chatPanelRef.current.aiDraftService = framework.get(AIDraftService);
|
||||
chatPanelRef.current.aiToolsConfigService =
|
||||
framework.get(AIToolsConfigService);
|
||||
chatPanelRef.current.subscriptionService =
|
||||
framework.get(SubscriptionService);
|
||||
chatPanelRef.current.aiModelService = framework.get(AIModelService);
|
||||
chatPanelRef.current.onAISubscribe = handleAISubscribe;
|
||||
|
||||
containerRef.current?.append(chatPanelRef.current);
|
||||
} else {
|
||||
chatPanelRef.current.host = editor.host;
|
||||
chatPanelRef.current.doc = editor.doc;
|
||||
if (nextSession === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSession(nextSession);
|
||||
setHasPinned(!!nextSession?.pinned);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [doc, workbench]);
|
||||
|
||||
const createSession = useCallback(
|
||||
async (options: Partial<BlockSuitePresets.AICreateSessionOptions> = {}) => {
|
||||
if (session || !AIProvider.session || !doc) {
|
||||
return session ?? undefined;
|
||||
}
|
||||
const sessionId = await AIProvider.session.createSession({
|
||||
docId: doc.id,
|
||||
workspaceId: doc.workspace.id,
|
||||
promptName: 'Chat With AFFiNE AI',
|
||||
reuseLatestChat: false,
|
||||
...options,
|
||||
});
|
||||
if (sessionId) {
|
||||
const nextSession = await AIProvider.session.getSession(
|
||||
doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
setSession(nextSession ?? null);
|
||||
return nextSession ?? undefined;
|
||||
}
|
||||
return session ?? undefined;
|
||||
},
|
||||
[doc, session]
|
||||
);
|
||||
|
||||
const updateSession = useCallback(
|
||||
async (options: UpdateChatSessionInput) => {
|
||||
if (!AIProvider.session || !doc) {
|
||||
return undefined;
|
||||
}
|
||||
await AIProvider.session.updateSession(options);
|
||||
const nextSession = await AIProvider.session.getSession(
|
||||
doc.workspace.id,
|
||||
options.sessionId
|
||||
);
|
||||
setSession(nextSession ?? null);
|
||||
return nextSession ?? undefined;
|
||||
},
|
||||
[doc]
|
||||
);
|
||||
|
||||
const newSession = useCallback(() => {
|
||||
resetPanel();
|
||||
requestAnimationFrame(() => {
|
||||
setSession(null);
|
||||
});
|
||||
}, [resetPanel]);
|
||||
|
||||
const openSession = useCallback(
|
||||
async (sessionId: string) => {
|
||||
if (session?.sessionId === sessionId || !AIProvider.session || !doc) {
|
||||
return;
|
||||
}
|
||||
resetPanel();
|
||||
const nextSession = await AIProvider.session.getSession(
|
||||
doc.workspace.id,
|
||||
sessionId
|
||||
);
|
||||
setSession(nextSession ?? null);
|
||||
},
|
||||
[doc, resetPanel, session?.sessionId]
|
||||
);
|
||||
|
||||
const openDoc = useCallback(
|
||||
async (docId: string, sessionId?: string) => {
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
if (doc.id === docId) {
|
||||
if (session?.sessionId === sessionId || session?.pinned) {
|
||||
return;
|
||||
}
|
||||
if (sessionId) {
|
||||
await openSession(sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (session?.pinned || !sessionId) {
|
||||
workbench.open(`/${docId}`, { at: 'active' });
|
||||
return;
|
||||
}
|
||||
workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' });
|
||||
},
|
||||
[doc, openSession, session?.pinned, session?.sessionId, workbench]
|
||||
);
|
||||
|
||||
const deleteSession = useCallback(
|
||||
async (sessionToDelete: BlockSuitePresets.AIRecentSession) => {
|
||||
if (!AIProvider.histories) {
|
||||
return;
|
||||
}
|
||||
const confirm = await notificationService.confirm({
|
||||
title: 'Delete this history?',
|
||||
message:
|
||||
'Do you want to delete this AI conversation history? Once deleted, it cannot be recovered.',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
});
|
||||
if (confirm) {
|
||||
await AIProvider.histories.cleanup(
|
||||
sessionToDelete.workspaceId,
|
||||
sessionToDelete.docId || undefined,
|
||||
[sessionToDelete.sessionId]
|
||||
);
|
||||
if (sessionToDelete.sessionId === session?.sessionId) {
|
||||
newSession();
|
||||
}
|
||||
}
|
||||
},
|
||||
[newSession, notificationService, session?.sessionId]
|
||||
);
|
||||
|
||||
const togglePin = useCallback(async () => {
|
||||
const pinned = !session?.pinned;
|
||||
setHasPinned(true);
|
||||
if (!session) {
|
||||
await createSession({ pinned });
|
||||
return;
|
||||
}
|
||||
setSession(prev => (prev ? { ...prev, pinned } : prev));
|
||||
await updateSession({
|
||||
sessionId: session.sessionId,
|
||||
pinned,
|
||||
});
|
||||
}, [createSession, session, updateSession]);
|
||||
|
||||
const rebindSession = useCallback(async () => {
|
||||
if (!session || !doc) {
|
||||
return;
|
||||
}
|
||||
if (session.docId !== doc.id) {
|
||||
await updateSession({
|
||||
sessionId: session.sessionId,
|
||||
docId: doc.id,
|
||||
});
|
||||
}
|
||||
}, [doc, session, updateSession]);
|
||||
|
||||
const onEmbeddingProgressChange = useCallback(
|
||||
(count: Record<ContextEmbedStatus, number>) => {
|
||||
const total = count.finished + count.processing + count.failed;
|
||||
setEmbeddingProgress([count.finished, total]);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onContextChange = useCallback(
|
||||
(context: Partial<ChatContextValue>) => {
|
||||
setStatus(context.status ?? 'idle');
|
||||
if (context.status === 'success') {
|
||||
rebindSession().catch(console.error);
|
||||
}
|
||||
},
|
||||
[rebindSession]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (session !== undefined) {
|
||||
return;
|
||||
}
|
||||
if (chatContent) {
|
||||
chatContent.remove();
|
||||
setChatContent(null);
|
||||
}
|
||||
if (chatToolbar) {
|
||||
chatToolbar.remove();
|
||||
setChatToolbar(null);
|
||||
}
|
||||
}, [chatContent, chatToolbar, session]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AIProvider.slots.userInfo.subscribe(() => {
|
||||
resetPanel();
|
||||
initPanel().catch(console.error);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [initPanel, resetPanel]);
|
||||
|
||||
useEffect(() => {
|
||||
const docId = doc?.id;
|
||||
if (!docId) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
lastDocIdRef.current &&
|
||||
lastDocIdRef.current !== docId &&
|
||||
!session?.pinned
|
||||
) {
|
||||
resetPanel();
|
||||
}
|
||||
lastDocIdRef.current = docId;
|
||||
}, [doc?.id, resetPanel, session?.pinned]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doc || session !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const tryInit = () => {
|
||||
if (cancelled || session !== undefined) {
|
||||
return;
|
||||
}
|
||||
// Session service may be registered after the panel mounts.
|
||||
if (AIProvider.session) {
|
||||
initPanel().catch(console.error);
|
||||
return;
|
||||
}
|
||||
timerId = setTimeout(tryInit, 200);
|
||||
};
|
||||
|
||||
tryInit();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
};
|
||||
}, [doc, initPanel, session]);
|
||||
|
||||
const contentKey = hasPinned
|
||||
? (session?.sessionId ?? doc?.id ?? 'chat-panel')
|
||||
: (doc?.id ?? 'chat-panel');
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatContent) {
|
||||
contentKeyRef.current = contentKey;
|
||||
return;
|
||||
}
|
||||
if (contentKeyRef.current && contentKeyRef.current !== contentKey) {
|
||||
chatContent.remove();
|
||||
setChatContent(null);
|
||||
}
|
||||
contentKeyRef.current = contentKey;
|
||||
}, [chatContent, contentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBodyProvided || !chatContainerRef.current || !doc || !host) {
|
||||
return;
|
||||
}
|
||||
if (session === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = chatContent;
|
||||
|
||||
if (!content) {
|
||||
content = new AIChatContent();
|
||||
}
|
||||
|
||||
content.host = host;
|
||||
content.session = session;
|
||||
content.createSession = createSession;
|
||||
content.workspaceId = doc.workspace.id;
|
||||
content.docId = doc.id;
|
||||
content.reasoningConfig = reasoningConfig;
|
||||
content.searchMenuConfig = searchMenuConfig;
|
||||
content.docDisplayConfig = docDisplayConfig;
|
||||
content.extensions = specs;
|
||||
content.serverService = framework.get(ServerService);
|
||||
content.affineFeatureFlagService = framework.get(FeatureFlagService);
|
||||
content.affineWorkspaceDialogService = framework.get(
|
||||
WorkspaceDialogService
|
||||
);
|
||||
content.affineThemeService = framework.get(AppThemeService);
|
||||
content.notificationService = notificationService;
|
||||
content.aiDraftService = framework.get(AIDraftService);
|
||||
content.aiToolsConfigService = framework.get(AIToolsConfigService);
|
||||
content.peekViewService = framework.get(PeekViewService);
|
||||
content.subscriptionService = framework.get(SubscriptionService);
|
||||
content.aiModelService = framework.get(AIModelService);
|
||||
content.onAISubscribe = handleAISubscribe;
|
||||
content.onEmbeddingProgressChange = onEmbeddingProgressChange;
|
||||
content.onContextChange = onContextChange;
|
||||
content.width = sidebarWidthSignal;
|
||||
content.onOpenDoc = (docId: string, sessionId?: string) => {
|
||||
openDoc(docId, sessionId).catch(console.error);
|
||||
};
|
||||
|
||||
if (!chatContent) {
|
||||
chatContainerRef.current.append(content);
|
||||
setChatContent(content);
|
||||
onLoad?.(content);
|
||||
}
|
||||
}, [
|
||||
chatContent,
|
||||
createSession,
|
||||
doc,
|
||||
docDisplayConfig,
|
||||
framework,
|
||||
handleAISubscribe,
|
||||
host,
|
||||
isBodyProvided,
|
||||
notificationService,
|
||||
onContextChange,
|
||||
onEmbeddingProgressChange,
|
||||
onLoad,
|
||||
openDoc,
|
||||
reasoningConfig,
|
||||
searchMenuConfig,
|
||||
session,
|
||||
sidebarWidthSignal,
|
||||
specs,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHeaderProvided || !chatToolbarContainerRef.current || !doc) {
|
||||
return;
|
||||
}
|
||||
if (session === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tool = chatToolbar;
|
||||
|
||||
if (!tool) {
|
||||
tool = new AIChatToolbar();
|
||||
}
|
||||
|
||||
tool.session = session;
|
||||
tool.workspaceId = doc.workspace.id;
|
||||
tool.docId = doc.id;
|
||||
tool.status = status;
|
||||
tool.docDisplayConfig = docDisplayConfig;
|
||||
tool.notificationService = notificationService;
|
||||
tool.onNewSession = newSession;
|
||||
tool.onTogglePin = togglePin;
|
||||
tool.onOpenSession = (sessionId: string) => {
|
||||
openSession(sessionId).catch(console.error);
|
||||
};
|
||||
tool.onOpenDoc = (docId: string, sessionId: string) => {
|
||||
openDoc(docId, sessionId).catch(console.error);
|
||||
};
|
||||
tool.onSessionDelete = (
|
||||
sessionToDelete: BlockSuitePresets.AIRecentSession
|
||||
) => {
|
||||
deleteSession(sessionToDelete).catch(console.error);
|
||||
};
|
||||
|
||||
if (!chatToolbar) {
|
||||
chatToolbarContainerRef.current.append(tool);
|
||||
setChatToolbar(tool);
|
||||
}
|
||||
}, [
|
||||
chatToolbar,
|
||||
deleteSession,
|
||||
doc,
|
||||
docDisplayConfig,
|
||||
isHeaderProvided,
|
||||
newSession,
|
||||
notificationService,
|
||||
openDoc,
|
||||
openSession,
|
||||
session,
|
||||
status,
|
||||
togglePin,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor?.host || !chatContent) {
|
||||
return;
|
||||
}
|
||||
const docModeService = editor.host.std.get(DocModeProvider);
|
||||
const refNodeService = editor.host.std.getOptional(RefNodeSlotsProvider);
|
||||
const disposable = [
|
||||
refNodeService?.docLinkClicked.subscribe(({ host }) => {
|
||||
if (host === editor.host) {
|
||||
(chatPanelRef.current as ChatPanel).doc = editor.doc;
|
||||
refNodeService?.docLinkClicked.subscribe(({ host: clickedHost }) => {
|
||||
if (clickedHost === editor.host) {
|
||||
chatContent.docId = editor.doc.id;
|
||||
}
|
||||
}),
|
||||
docModeService?.onPrimaryModeChange(() => {
|
||||
if (!editor.host) return;
|
||||
(chatPanelRef.current as ChatPanel).host = editor.host;
|
||||
if (!editor.host) {
|
||||
return;
|
||||
}
|
||||
chatContent.host = editor.host;
|
||||
}, editor.doc.id),
|
||||
];
|
||||
|
||||
return () => disposable.forEach(d => d?.unsubscribe());
|
||||
}, [
|
||||
docDisplayConfig,
|
||||
editor,
|
||||
framework,
|
||||
searchMenuConfig,
|
||||
reasoningConfig,
|
||||
playgroundConfig,
|
||||
confirmModal,
|
||||
specs,
|
||||
handleAISubscribe,
|
||||
]);
|
||||
return () => disposable.forEach(item => item?.unsubscribe());
|
||||
}, [chatContent, editor]);
|
||||
|
||||
const [autoResized, setAutoResized] = useState(false);
|
||||
useEffect(() => {
|
||||
// after auto expanded first time, do not auto expand again(even if user manually resized)
|
||||
if (autoResized) return;
|
||||
if (autoResized) {
|
||||
return;
|
||||
}
|
||||
const subscription = AIProvider.slots.previewPanelOpenChange.subscribe(
|
||||
open => {
|
||||
if (!open) return;
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
const sidebarWidth = workbench.sidebarWidth$.value;
|
||||
const MIN_SIDEBAR_WIDTH = 1080;
|
||||
if (!sidebarWidth || sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
||||
workbench.setSidebarWidth(MIN_SIDEBAR_WIDTH);
|
||||
const minSidebarWidth = 1080;
|
||||
if (!sidebarWidth || sidebarWidth < minSidebarWidth) {
|
||||
workbench.setSidebarWidth(minSidebarWidth);
|
||||
setAutoResized(true);
|
||||
}
|
||||
}
|
||||
@@ -165,5 +551,99 @@ export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
};
|
||||
}, [autoResized, workbench]);
|
||||
|
||||
return <div className={styles.root} ref={containerRef} />;
|
||||
});
|
||||
const openPlayground = useCallback(() => {
|
||||
if (!doc || !host) {
|
||||
return;
|
||||
}
|
||||
const playgroundContent = html`
|
||||
<playground-content
|
||||
.host=${host}
|
||||
.doc=${doc}
|
||||
.reasoningConfig=${reasoningConfig}
|
||||
.playgroundConfig=${playgroundConfig}
|
||||
.appSidebarConfig=${appSidebarConfig}
|
||||
.searchMenuConfig=${searchMenuConfig}
|
||||
.docDisplayConfig=${docDisplayConfig}
|
||||
.extensions=${specs}
|
||||
.serverService=${framework.get(ServerService)}
|
||||
.affineFeatureFlagService=${framework.get(FeatureFlagService)}
|
||||
.affineThemeService=${framework.get(AppThemeService)}
|
||||
.notificationService=${notificationService}
|
||||
.affineWorkspaceDialogService=${framework.get(WorkspaceDialogService)}
|
||||
.aiToolsConfigService=${framework.get(AIToolsConfigService)}
|
||||
.subscriptionService=${framework.get(SubscriptionService)}
|
||||
.aiModelService=${framework.get(AIModelService)}
|
||||
></playground-content>
|
||||
`;
|
||||
|
||||
createPlaygroundModal(playgroundContent, 'AI Playground');
|
||||
}, [
|
||||
appSidebarConfig,
|
||||
doc,
|
||||
docDisplayConfig,
|
||||
framework,
|
||||
host,
|
||||
notificationService,
|
||||
playgroundConfig,
|
||||
reasoningConfig,
|
||||
searchMenuConfig,
|
||||
specs,
|
||||
]);
|
||||
|
||||
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
setIsBodyProvided(true);
|
||||
chatContainerRef.current = node;
|
||||
}, []);
|
||||
|
||||
const onChatToolContainerRef = useCallback((node: HTMLDivElement) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
setIsHeaderProvided(true);
|
||||
chatToolbarContainerRef.current = node;
|
||||
}, []);
|
||||
|
||||
const isEmbedding =
|
||||
embeddingProgress[1] > 0 && embeddingProgress[0] < embeddingProgress[1];
|
||||
const [done, total] = embeddingProgress;
|
||||
const isInitialized = session !== undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{!isInitialized ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.loading}>
|
||||
<Logo1Icon className={styles.loadingIcon} />
|
||||
<div className={styles.loadingTitle}>
|
||||
AFFiNE AI is loading history...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
{isEmbedding ? (
|
||||
<span data-testid="chat-panel-embedding-progress">
|
||||
Embedding {done}/{total}
|
||||
</span>
|
||||
) : (
|
||||
'AFFiNE AI'
|
||||
)}
|
||||
</div>
|
||||
{playgroundVisible ? (
|
||||
<div className={styles.playground} onClick={openPlayground}>
|
||||
<CenterPeekIcon />
|
||||
</div>
|
||||
) : null}
|
||||
<div ref={onChatToolContainerRef} />
|
||||
</div>
|
||||
<div className={styles.content} ref={onChatContainerRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { toReactNode } from '@affine/component';
|
||||
import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/ai';
|
||||
import type { AIChatBlockModel } from '@affine/core/blocksuite/ai/blocks/ai-chat-block/model/ai-chat-model';
|
||||
import { registerAIAppEffects } from '@affine/core/blocksuite/ai/effects/app';
|
||||
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
||||
import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe';
|
||||
import {
|
||||
@@ -15,6 +16,8 @@ import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { useFramework } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
registerAIAppEffects();
|
||||
|
||||
export type AIChatBlockPeekViewProps = {
|
||||
model: AIChatBlockModel;
|
||||
host: EditorHost;
|
||||
|
||||
@@ -13,5 +13,5 @@ test('Click ai-land icon', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await page.locator('[data-testid=ai-island]').click();
|
||||
|
||||
await expect(page.locator('chat-panel')).toBeVisible();
|
||||
await expect(page.getByTestId('chat-panel-input-container')).toBeVisible();
|
||||
});
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@@ -457,6 +457,7 @@ __metadata:
|
||||
fuse.js: "npm:^7.0.0"
|
||||
graphemer: "npm:^1.4.0"
|
||||
graphql: "npm:^16.9.0"
|
||||
happy-dom: "npm:^20.3.0"
|
||||
history: "npm:^5.3.0"
|
||||
idb: "npm:^8.0.0"
|
||||
idb-keyval: "npm:^6.2.2"
|
||||
@@ -18961,7 +18962,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10":
|
||||
"@types/ws@npm:^8.0.0, @types/ws@npm:^8.18.1, @types/ws@npm:^8.5.10":
|
||||
version: 8.18.1
|
||||
resolution: "@types/ws@npm:8.18.1"
|
||||
dependencies:
|
||||
@@ -26694,14 +26695,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"happy-dom@npm:^20.0.0":
|
||||
version: 20.0.7
|
||||
resolution: "happy-dom@npm:20.0.7"
|
||||
"happy-dom@npm:^20.0.0, happy-dom@npm:^20.3.0":
|
||||
version: 20.3.0
|
||||
resolution: "happy-dom@npm:20.3.0"
|
||||
dependencies:
|
||||
"@types/node": "npm:^20.0.0"
|
||||
"@types/whatwg-mimetype": "npm:^3.0.2"
|
||||
"@types/ws": "npm:^8.18.1"
|
||||
whatwg-mimetype: "npm:^3.0.0"
|
||||
checksum: 10/1161bfe8fcac0fd093b8fbe8af29e8a6525437f17424be97b21401f0741e6473541b30b080066a32990b884eae4b83a1fd1f948a52607975bc98d1b90b394509
|
||||
ws: "npm:^8.18.3"
|
||||
checksum: 10/aade5560110eeaad502679a314f4d3b8658fe4697c1914d0de09ec6a176611ad0c8953f0aa559c56d1afabe0ab8465cde7b4811006df6eb3b54c5e5ea4169b29
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -38868,9 +38871,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.18.2":
|
||||
version: 8.18.3
|
||||
resolution: "ws@npm:8.18.3"
|
||||
"ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.18.2, ws@npm:^8.18.3":
|
||||
version: 8.19.0
|
||||
resolution: "ws@npm:8.19.0"
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ">=5.0.2"
|
||||
@@ -38879,7 +38882,7 @@ __metadata:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6
|
||||
checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user