mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user