mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(core): add ai-chat-toolbar for independent chat (#13021)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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%',
|
||||
});
|
||||
|
||||
@@ -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<ReturnType<CopilotClient['getSession']>>;
|
||||
|
||||
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<Doc | null>(null);
|
||||
const [isHeaderProvided, setIsHeaderProvided] = useState(false);
|
||||
const [host, setHost] = useState<EditorHost | null>(null);
|
||||
const [chatContent, setChatContent] = useState<AIChatContent | null>(null);
|
||||
const [chatTool, setChatTool] = useState<AIChatToolbar | null>(null);
|
||||
const [currentSession, setCurrentSession] = useState<CopilotSession | null>(
|
||||
null
|
||||
);
|
||||
const [isTogglingPin, setIsTogglingPin] = useState(false);
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const chatToolContainerRef = useRef<HTMLDivElement>(null);
|
||||
const widthSignalRef = useRef<Signal<number>>(signal(0));
|
||||
const client = useCopilotClient();
|
||||
|
||||
@@ -64,6 +73,42 @@ export const Component = () => {
|
||||
reasoningConfig,
|
||||
} = useAIChatConfig();
|
||||
|
||||
const createSession = useCallback(
|
||||
async (options: Partial<BlockSuitePresets.AICreateSessionOptions> = {}) => {
|
||||
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 = () => {
|
||||
<>
|
||||
<ViewTitle title={t['AFFiNE AI']()} />
|
||||
<ViewIcon icon="ai" />
|
||||
<ViewHeader></ViewHeader>
|
||||
<ViewHeader>
|
||||
<div className={styles.chatHeader}>
|
||||
<div />
|
||||
<div ref={onChatToolContainerRef} />
|
||||
</div>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.chatRoot} ref={onChatContainerRef} />
|
||||
</ViewBody>
|
||||
|
||||
Reference in New Issue
Block a user