Compare commits

...

1 Commits

Author SHA1 Message Date
Wu Yue
b85afa7394 refactor(core): extract ai-chat-panel-title component (#13209)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a dedicated AI chat panel title bar with dynamic embedding
progress display and an optional playground button.
* Added a modal playground interface accessible from the chat panel
title when enabled.

* **Refactor**
* Moved the chat panel title and related UI logic into a new, reusable
component for improved modularity.
* Simplified the chat content area by removing the internal chat title
rendering and related methods.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 02:56:57 +00:00
4 changed files with 213 additions and 123 deletions

View File

@@ -0,0 +1,186 @@
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 {
AINetworkSearchConfig,
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);
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 networkSearchConfig!: AINetworkSearchConfig;
@property({ attribute: false })
accessor reasoningConfig!: AIReasoningConfig;
@property({ attribute: false })
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
accessor extensions!: ExtensionType[];
@property({ attribute: false })
accessor affineFeatureFlagService!: FeatureFlagService;
@property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor affineThemeService!: AppThemeService;
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor session!: CopilotChatHistoryFragment;
@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;
private readonly openPlayground = () => {
const playgroundContent = html`
<playground-content
.host=${this.host}
.doc=${this.doc}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig}
.appSidebarConfig=${this.appSidebarConfig}
.searchMenuConfig=${this.searchMenuConfig}
.docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
></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}
.docDisplayConfig=${this.docDisplayConfig}
.notificationService=${this.notificationService}
></ai-chat-toolbar>
</div>
`;
}
}

View File

@@ -9,13 +9,11 @@ import type {
} 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 { type Signal, signal } from '@preact/signals-core';
import { css, html, nothing, type PropertyValues } from 'lit';
import { css, html, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js';
import { keyed } from 'lit/directives/keyed.js';
@@ -29,7 +27,6 @@ import type {
AIReasoningConfig,
} from '../components/ai-chat-input';
import type { ChatStatus } from '../components/ai-chat-messages';
import { createPlaygroundModal } from '../components/playground/modal';
import { AIProvider } from '../provider';
import type { AppSidebarConfig } from './chat-config';
@@ -45,12 +42,6 @@ export class ChatPanel extends SignalWatcher(
height: 100%;
}
.chat-panel-title-text {
font-size: 14px;
font-weight: 500;
color: var(--affine-text-secondary-color);
}
.chat-loading-container {
position: relative;
padding: 44px 0 166px 0;
@@ -72,20 +63,6 @@ export class ChatPanel extends SignalWatcher(
font-size: var(--affine-font-sm);
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')};
}
}
`;
@@ -150,40 +127,6 @@ export class ChatPanel extends SignalWatcher(
return this.session !== undefined;
}
private get chatTitle() {
const [done, total] = this.embeddingProgress;
const isEmbedding = total > 0 && done < total;
return html`
<div class="chat-panel-title-text">
${isEmbedding
? html`<span data-testid="chat-panel-embedding-progress"
>Embedding ${done}/${total}</span
>`
: 'AFFiNE AI'}
</div>
${this.playgroundConfig.visible.value
? html`
<div class="chat-panel-playground" @click=${this.openPlayground}>
${CenterPeekIcon()}
</div>
`
: nothing}
<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}
.docDisplayConfig=${this.docDisplayConfig}
.notificationService=${this.notificationService}
></ai-chat-toolbar>
`;
}
private readonly getSessionIdFromUrl = () => {
if (this.affineWorkbenchService) {
const { workbench } = this.affineWorkbenchService;
@@ -368,28 +311,6 @@ export class ChatPanel extends SignalWatcher(
}
};
private readonly openPlayground = () => {
const playgroundContent = html`
<playground-content
.host=${this.host}
.doc=${this.doc}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.playgroundConfig=${this.playgroundConfig}
.appSidebarConfig=${this.appSidebarConfig}
.searchMenuConfig=${this.searchMenuConfig}
.docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
></playground-content>
`;
createPlaygroundModal(playgroundContent, 'AI Playground');
};
protected override updated(changedProperties: PropertyValues) {
if (changedProperties.has('doc')) {
if (this.session?.pinned) {
@@ -441,10 +362,31 @@ export class ChatPanel extends SignalWatcher(
}
return html`<div class="chat-panel-container">
<ai-chat-panel-title
.host=${this.host}
.doc=${this.doc}
.playgroundConfig=${this.playgroundConfig}
.appSidebarConfig=${this.appSidebarConfig}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.searchMenuConfig=${this.searchMenuConfig}
.docDisplayConfig=${this.docDisplayConfig}
.extensions=${this.extensions}
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.session=${this.session}
.status=${this.status}
.embeddingProgress=${this.embeddingProgress}
.newSession=${this.newSession}
.togglePin=${this.togglePin}
.openSession=${this.openSession}
.openDoc=${this.openDoc}
></ai-chat-panel-title>
${keyed(
this.hasPinned ? this.session?.sessionId : this.doc.id,
html`<ai-chat-content
.chatTitle=${this.chatTitle}
.host=${this.host}
.session=${this.session}
.createSession=${this.createSession}

View File

@@ -10,13 +10,7 @@ import { type EditorHost, ShadowlessElement } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import { type Signal } from '@preact/signals-core';
import {
css,
html,
nothing,
type PropertyValues,
type TemplateResult,
} from 'lit';
import { css, html, type PropertyValues, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
@@ -60,24 +54,6 @@ export class AIChatContent extends SignalWatcher(
justify-content: center;
height: 100%;
.ai-chat-title {
background: var(--affine-background-primary-color);
position: relative;
padding: 8px var(--h-padding);
width: 100%;
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1;
svg {
width: 18px;
height: 18px;
color: var(--affine-text-secondary-color);
}
}
ai-chat-messages {
flex: 1;
overflow-y: auto;
@@ -129,9 +105,6 @@ export class AIChatContent extends SignalWatcher(
@property({ attribute: false })
accessor onboardingOffsetY!: number;
@property({ attribute: false })
accessor chatTitle: TemplateResult<1> | undefined;
@property({ attribute: false })
accessor host: EditorHost | null | undefined;
@@ -328,16 +301,6 @@ export class AIChatContent extends SignalWatcher(
}
}
public reset() {
this.updateContext(DEFAULT_CHAT_CONTEXT_VALUE);
this.closePreviewPanel(true);
}
public reloadSession() {
this.reset();
this.initChatContent().catch(console.error);
}
public openPreviewPanel(content?: TemplateResult<1>) {
this.showPreviewPanel = true;
if (content) this.previewPanelContent = content;
@@ -390,10 +353,7 @@ export class AIChatContent extends SignalWatcher(
}
override render() {
const left = html`${this.chatTitle
? html`<div class="ai-chat-title">${this.chatTitle}</div>`
: nothing}
<ai-chat-messages
const left = html` <ai-chat-messages
class=${classMap({
'ai-chat-messages': true,
'independent-mode': !!this.independentMode,

View File

@@ -22,6 +22,7 @@ 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';
@@ -141,6 +142,7 @@ export function registerAIEffects() {
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(