From 2b0b20cdd490ab0bc930f5ef2c0542fdf2074e9f Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Fri, 4 Jul 2025 13:16:20 +0800 Subject: [PATCH] feat(core): add ai-chat-toolbar for independent chat (#13021) ## Summary by CodeRabbit * **New Features** * Introduced an AI chat toolbar for improved session management and interaction. * Added the ability to pin chat sessions and reset chat content. * Enhanced chat header layout for better usability. * **Improvements** * Streamlined session creation and management within the AI chat interface. --- .../ai-chat-content/ai-chat-content.ts | 4 + .../desktop/pages/workspace/chat/index.css.ts | 7 ++ .../desktop/pages/workspace/chat/index.tsx | 109 +++++++++++++++--- 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts index 800aeeeb64..94b051a983 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.ts @@ -310,6 +310,10 @@ export class AIChatContent extends SignalWatcher( } } + public reset() { + this.updateContext(DEFAULT_CHAT_CONTEXT_VALUE); + } + override connectedCallback() { super.connectedCallback(); this.initChatContent().catch(console.error); diff --git a/packages/frontend/core/src/desktop/pages/workspace/chat/index.css.ts b/packages/frontend/core/src/desktop/pages/workspace/chat/index.css.ts index 86ef17d144..83b0611818 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.css.ts @@ -7,3 +7,10 @@ export const chatRoot = style({ padding: '0px 16px', margin: '0 auto', }); + +export const chatHeader = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx index d1d3dbafbe..acf4471039 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx @@ -1,6 +1,7 @@ import { observeResize } from '@affine/component'; import { CopilotClient } from '@affine/core/blocksuite/ai'; import { AIChatContent } from '@affine/core/blocksuite/ai/components/ai-chat-content'; +import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar'; import { getCustomPageEditorBlockSpecs } from '@affine/core/blocksuite/ai/components/text-renderer'; import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt'; import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config'; @@ -28,6 +29,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import * as styles from './index.css'; +type CopilotSession = Awaited>; + function useCopilotClient() { const graphqlService = useService(GraphQLService); const eventSourceService = useService(EventSourceService); @@ -48,10 +51,16 @@ export const Component = () => { const t = useI18n(); const framework = useFramework(); const [isBodyProvided, setIsBodyProvided] = useState(false); - const [doc, setDoc] = useState(null); + const [isHeaderProvided, setIsHeaderProvided] = useState(false); const [host, setHost] = useState(null); const [chatContent, setChatContent] = useState(null); + const [chatTool, setChatTool] = useState(null); + const [currentSession, setCurrentSession] = useState( + null + ); + const [isTogglingPin, setIsTogglingPin] = useState(false); const chatContainerRef = useRef(null); + const chatToolContainerRef = useRef(null); const widthSignalRef = useRef>(signal(0)); const client = useCopilotClient(); @@ -64,6 +73,42 @@ export const Component = () => { reasoningConfig, } = useAIChatConfig(); + const createSession = useCallback( + async (options: Partial = {}) => { + const sessionId = await client.createSession({ + workspaceId, + promptName: 'Chat With AFFiNE AI' satisfies PromptKey, + ...options, + }); + + const session = await client.getSession(workspaceId, sessionId); + setCurrentSession(session); + return session; + }, + [client, workspaceId] + ); + + const togglePin = useCallback(async () => { + if (isTogglingPin) return; + setIsTogglingPin(true); + try { + const pinned = !currentSession?.pinned; + if (!currentSession) { + await createSession({ pinned }); + } else { + await client.updateSession({ + sessionId: currentSession.id, + pinned, + }); + // retrieve the latest session and update the state + const session = await client.getSession(workspaceId, currentSession.id); + setCurrentSession(session); + } + } finally { + setIsTogglingPin(false); + } + }, [client, createSession, currentSession, isTogglingPin, workspaceId]); + // create a temp doc/host for ai-chat-content useEffect(() => { let tempDoc: Doc | null = null; @@ -75,7 +120,6 @@ export const Component = () => { store: tempDoc?.getStore() as Store, extensions: getCustomPageEditorBlockSpecs(), }).render(); - setDoc(doc); setHost(host); }); @@ -86,7 +130,7 @@ export const Component = () => { // init or update ai-chat-content useEffect(() => { - if (!isBodyProvided || !host || !doc) { + if (!isBodyProvided || !host) { return; } @@ -95,6 +139,8 @@ export const Component = () => { if (!content) { content = new AIChatContent(); } + + content.session = currentSession; content.host = host; content.workspaceId = workspaceId; content.docDisplayConfig = docDisplayConfig; @@ -108,17 +154,6 @@ export const Component = () => { if (!chatContent) { // initial values that won't change - const createSession = async () => { - const sessionId = await client.createSession({ - workspaceId, - docId: doc.id, - promptName: 'Chat With AFFiNE AI' satisfies PromptKey, - }); - - const session = await client.getSession(workspaceId, sessionId); - return session; - }; - content.createSession = createSession; content.independentMode = true; content.onboardingOffsetY = -100; @@ -128,7 +163,8 @@ export const Component = () => { }, [ chatContent, client, - doc, + createSession, + currentSession, docDisplayConfig, framework, host, @@ -139,6 +175,35 @@ export const Component = () => { workspaceId, ]); + // init or update header ai-chat-toolbar + useEffect(() => { + if (!isHeaderProvided || !chatToolContainerRef.current || !chatContent) { + return; + } + let tool = chatTool; + + if (!tool) { + tool = new AIChatToolbar(); + } + + tool.session = currentSession; + + // initial props + if (!chatTool) { + tool.onNewSession = () => { + if (!currentSession) return; + setCurrentSession(null); + chatContent?.reset(); + }; + tool.onTogglePin = () => { + togglePin().catch(console.error); + }; + // mount + chatToolContainerRef.current.append(tool); + setChatTool(tool); + } + }, [chatContent, chatTool, currentSession, isHeaderProvided, togglePin]); + const onChatContainerRef = useCallback((node: HTMLDivElement) => { if (node) { setIsBodyProvided(true); @@ -147,6 +212,13 @@ export const Component = () => { } }, []); + const onChatToolContainerRef = useCallback((node: HTMLDivElement) => { + if (node) { + setIsHeaderProvided(true); + chatToolContainerRef.current = node; + } + }, []); + // observe chat container width and provide to ai-chat-content useEffect(() => { if (!isBodyProvided || !chatContainerRef.current) return; @@ -159,7 +231,12 @@ export const Component = () => { <> - + +
+
+
+
+