feat(core): disable pin chat while generating AI answers (#13131)

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

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

## Summary by CodeRabbit

* **New Features**
* Chat status is now displayed and updated in the chat panel and
toolbar, allowing users to see when the chat is generating a response.
* The pin button in the chat toolbar is disabled while the chat is
generating a response, preventing pin actions during this time and
providing feedback via a notification if attempted.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-10 12:25:12 +08:00
committed by GitHub
parent 385226083f
commit fe00293e3e
3 changed files with 47 additions and 2 deletions

View File

@@ -30,6 +30,7 @@ import type {
AIPlaygroundConfig,
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';
@@ -138,6 +139,9 @@ export class ChatPanel extends SignalWatcher(
@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);
@@ -171,6 +175,7 @@ export class ChatPanel extends SignalWatcher(
.session=${this.session}
.workspaceId=${this.doc.workspace.id}
.docId=${this.doc.id}
.status=${this.status}
.onNewSession=${this.newSession}
.onTogglePin=${this.togglePin}
.onOpenSession=${this.openSession}
@@ -359,6 +364,7 @@ export class ChatPanel extends SignalWatcher(
private readonly onContextChange = async (
context: Partial<ChatContextValue>
) => {
this.status = context.status ?? 'idle';
if (context.status === 'success') {
await this.rebindSession();
}

View File

@@ -15,6 +15,7 @@ import { css, html } from 'lit';
import { property, query } from 'lit/decorators.js';
import type { DocDisplayConfig } from '../ai-chat-chips';
import type { ChatStatus } from '../ai-chat-messages';
export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
@@ -26,6 +27,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docId: string | undefined;
@property({ attribute: false })
accessor status!: ChatStatus;
@property({ attribute: false })
accessor onNewSession!: () => void;
@@ -49,6 +53,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
private abortController: AbortController | null = null;
get isGenerating() {
return this.status === 'transmitting' || this.status === 'loading';
}
static override styles = css`
.ai-chat-toolbar {
display: flex;
@@ -72,6 +80,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
height: 16px;
color: ${unsafeCSSVarV2('icon/primary')};
}
&[data-disabled='true'] {
cursor: not-allowed;
}
}
}
`;
@@ -84,7 +96,11 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
${PlusIcon()}
<affine-tooltip>New Chat</affine-tooltip>
</div>
<div class="chat-toolbar-icon" @click=${this.onTogglePin}>
<div
class="chat-toolbar-icon"
@click=${this.onPinClick}
data-disabled=${this.isGenerating}
>
${pinned ? PinedIcon() : PinIcon()}
<affine-tooltip>
${pinned ? 'Unpin this Chat' : 'Pin this Chat'}
@@ -101,6 +117,16 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
`;
}
private readonly onPinClick = async () => {
if (this.isGenerating) {
this.notificationService.toast(
'Cannot pin a chat while generating an answer'
);
return;
}
await this.onTogglePin();
};
private readonly unpinConfirm = async () => {
if (this.session && this.session.pinned) {
try {

View File

@@ -1,6 +1,10 @@
import { observeResize, useConfirmModal } from '@affine/component';
import { CopilotClient } from '@affine/core/blocksuite/ai';
import { AIChatContent } from '@affine/core/blocksuite/ai/components/ai-chat-content';
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 type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
@@ -55,6 +59,7 @@ export const Component = () => {
const [currentSession, setCurrentSession] = useState<CopilotSession | null>(
null
);
const [status, setStatus] = useState<ChatStatus>('idle');
const [isTogglingPin, setIsTogglingPin] = useState(false);
const [isOpeningSession, setIsOpeningSession] = useState(false);
const chatContainerRef = useRef<HTMLDivElement>(null);
@@ -129,6 +134,10 @@ export const Component = () => {
[chatContent, chatTool, client, isOpeningSession, workspaceId]
);
const onContextChange = useCallback((context: Partial<ChatContextValue>) => {
setStatus(context.status ?? 'idle');
}, []);
const confirmModal = useConfirmModal();
// init or update ai-chat-content
@@ -149,6 +158,7 @@ export const Component = () => {
content.searchMenuConfig = searchMenuConfig;
content.networkSearchConfig = networkSearchConfig;
content.reasoningConfig = reasoningConfig;
content.onContextChange = onContextChange;
content.affineFeatureFlagService = framework.get(FeatureFlagService);
content.affineWorkspaceDialogService = framework.get(
WorkspaceDialogService
@@ -180,6 +190,7 @@ export const Component = () => {
searchMenuConfig,
workspaceId,
confirmModal,
onContextChange,
]);
// init or update header ai-chat-toolbar
@@ -195,6 +206,7 @@ export const Component = () => {
tool.session = currentSession;
tool.workspaceId = workspaceId;
tool.status = status;
tool.docDisplayConfig = docDisplayConfig;
tool.onOpenSession = onOpenSession;
tool.notificationService = new NotificationServiceImpl(
@@ -237,6 +249,7 @@ export const Component = () => {
workspaceId,
confirmModal,
framework,
status,
]);
const onChatContainerRef = useCallback((node: HTMLDivElement) => {