mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08: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,
|
AIPlaygroundConfig,
|
||||||
AIReasoningConfig,
|
AIReasoningConfig,
|
||||||
} from '../components/ai-chat-input';
|
} from '../components/ai-chat-input';
|
||||||
|
import type { ChatStatus } from '../components/ai-chat-messages';
|
||||||
import { createPlaygroundModal } from '../components/playground/modal';
|
import { createPlaygroundModal } from '../components/playground/modal';
|
||||||
import { AIProvider } from '../provider';
|
import { AIProvider } from '../provider';
|
||||||
import type { AppSidebarConfig } from './chat-config';
|
import type { AppSidebarConfig } from './chat-config';
|
||||||
@@ -138,6 +139,9 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
@state()
|
@state()
|
||||||
accessor embeddingProgress: [number, number] = [0, 0];
|
accessor embeddingProgress: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor status: ChatStatus = 'idle';
|
||||||
|
|
||||||
private isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
private isSidebarOpen: Signal<boolean | undefined> = signal(false);
|
||||||
|
|
||||||
private sidebarWidth: Signal<number | undefined> = signal(undefined);
|
private sidebarWidth: Signal<number | undefined> = signal(undefined);
|
||||||
@@ -171,6 +175,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.session=${this.session}
|
.session=${this.session}
|
||||||
.workspaceId=${this.doc.workspace.id}
|
.workspaceId=${this.doc.workspace.id}
|
||||||
.docId=${this.doc.id}
|
.docId=${this.doc.id}
|
||||||
|
.status=${this.status}
|
||||||
.onNewSession=${this.newSession}
|
.onNewSession=${this.newSession}
|
||||||
.onTogglePin=${this.togglePin}
|
.onTogglePin=${this.togglePin}
|
||||||
.onOpenSession=${this.openSession}
|
.onOpenSession=${this.openSession}
|
||||||
@@ -359,6 +364,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
private readonly onContextChange = async (
|
private readonly onContextChange = async (
|
||||||
context: Partial<ChatContextValue>
|
context: Partial<ChatContextValue>
|
||||||
) => {
|
) => {
|
||||||
|
this.status = context.status ?? 'idle';
|
||||||
if (context.status === 'success') {
|
if (context.status === 'success') {
|
||||||
await this.rebindSession();
|
await this.rebindSession();
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-1
@@ -15,6 +15,7 @@ import { css, html } from 'lit';
|
|||||||
import { property, query } from 'lit/decorators.js';
|
import { property, query } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { DocDisplayConfig } from '../ai-chat-chips';
|
import type { DocDisplayConfig } from '../ai-chat-chips';
|
||||||
|
import type { ChatStatus } from '../ai-chat-messages';
|
||||||
|
|
||||||
export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -26,6 +27,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docId: string | undefined;
|
accessor docId: string | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor status!: ChatStatus;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onNewSession!: () => void;
|
accessor onNewSession!: () => void;
|
||||||
|
|
||||||
@@ -49,6 +53,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
private abortController: AbortController | null = null;
|
private abortController: AbortController | null = null;
|
||||||
|
|
||||||
|
get isGenerating() {
|
||||||
|
return this.status === 'transmitting' || this.status === 'loading';
|
||||||
|
}
|
||||||
|
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
.ai-chat-toolbar {
|
.ai-chat-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -72,6 +80,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
color: ${unsafeCSSVarV2('icon/primary')};
|
color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-disabled='true'] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -84,7 +96,11 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
|||||||
${PlusIcon()}
|
${PlusIcon()}
|
||||||
<affine-tooltip>New Chat</affine-tooltip>
|
<affine-tooltip>New Chat</affine-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-toolbar-icon" @click=${this.onTogglePin}>
|
<div
|
||||||
|
class="chat-toolbar-icon"
|
||||||
|
@click=${this.onPinClick}
|
||||||
|
data-disabled=${this.isGenerating}
|
||||||
|
>
|
||||||
${pinned ? PinedIcon() : PinIcon()}
|
${pinned ? PinedIcon() : PinIcon()}
|
||||||
<affine-tooltip>
|
<affine-tooltip>
|
||||||
${pinned ? 'Unpin this Chat' : 'Pin this Chat'}
|
${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 () => {
|
private readonly unpinConfirm = async () => {
|
||||||
if (this.session && this.session.pinned) {
|
if (this.session && this.session.pinned) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { observeResize, useConfirmModal } from '@affine/component';
|
import { observeResize, useConfirmModal } from '@affine/component';
|
||||||
import { CopilotClient } from '@affine/core/blocksuite/ai';
|
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 { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
|
||||||
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
|
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
|
||||||
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
|
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>(
|
const [currentSession, setCurrentSession] = useState<CopilotSession | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [status, setStatus] = useState<ChatStatus>('idle');
|
||||||
const [isTogglingPin, setIsTogglingPin] = useState(false);
|
const [isTogglingPin, setIsTogglingPin] = useState(false);
|
||||||
const [isOpeningSession, setIsOpeningSession] = useState(false);
|
const [isOpeningSession, setIsOpeningSession] = useState(false);
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -129,6 +134,10 @@ export const Component = () => {
|
|||||||
[chatContent, chatTool, client, isOpeningSession, workspaceId]
|
[chatContent, chatTool, client, isOpeningSession, workspaceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onContextChange = useCallback((context: Partial<ChatContextValue>) => {
|
||||||
|
setStatus(context.status ?? 'idle');
|
||||||
|
}, []);
|
||||||
|
|
||||||
const confirmModal = useConfirmModal();
|
const confirmModal = useConfirmModal();
|
||||||
|
|
||||||
// init or update ai-chat-content
|
// init or update ai-chat-content
|
||||||
@@ -149,6 +158,7 @@ export const Component = () => {
|
|||||||
content.searchMenuConfig = searchMenuConfig;
|
content.searchMenuConfig = searchMenuConfig;
|
||||||
content.networkSearchConfig = networkSearchConfig;
|
content.networkSearchConfig = networkSearchConfig;
|
||||||
content.reasoningConfig = reasoningConfig;
|
content.reasoningConfig = reasoningConfig;
|
||||||
|
content.onContextChange = onContextChange;
|
||||||
content.affineFeatureFlagService = framework.get(FeatureFlagService);
|
content.affineFeatureFlagService = framework.get(FeatureFlagService);
|
||||||
content.affineWorkspaceDialogService = framework.get(
|
content.affineWorkspaceDialogService = framework.get(
|
||||||
WorkspaceDialogService
|
WorkspaceDialogService
|
||||||
@@ -180,6 +190,7 @@ export const Component = () => {
|
|||||||
searchMenuConfig,
|
searchMenuConfig,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
confirmModal,
|
confirmModal,
|
||||||
|
onContextChange,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// init or update header ai-chat-toolbar
|
// init or update header ai-chat-toolbar
|
||||||
@@ -195,6 +206,7 @@ export const Component = () => {
|
|||||||
|
|
||||||
tool.session = currentSession;
|
tool.session = currentSession;
|
||||||
tool.workspaceId = workspaceId;
|
tool.workspaceId = workspaceId;
|
||||||
|
tool.status = status;
|
||||||
tool.docDisplayConfig = docDisplayConfig;
|
tool.docDisplayConfig = docDisplayConfig;
|
||||||
tool.onOpenSession = onOpenSession;
|
tool.onOpenSession = onOpenSession;
|
||||||
tool.notificationService = new NotificationServiceImpl(
|
tool.notificationService = new NotificationServiceImpl(
|
||||||
@@ -237,6 +249,7 @@ export const Component = () => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
confirmModal,
|
confirmModal,
|
||||||
framework,
|
framework,
|
||||||
|
status,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user