mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +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() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.initChatContent().catch(console.error);
|
this.initChatContent().catch(console.error);
|
||||||
|
|||||||
@@ -7,3 +7,10 @@ export const chatRoot = style({
|
|||||||
padding: '0px 16px',
|
padding: '0px 16px',
|
||||||
margin: '0 auto',
|
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 { observeResize } 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 } 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 { getCustomPageEditorBlockSpecs } from '@affine/core/blocksuite/ai/components/text-renderer';
|
||||||
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
|
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
|
||||||
import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-chat-config';
|
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';
|
import * as styles from './index.css';
|
||||||
|
|
||||||
|
type CopilotSession = Awaited<ReturnType<CopilotClient['getSession']>>;
|
||||||
|
|
||||||
function useCopilotClient() {
|
function useCopilotClient() {
|
||||||
const graphqlService = useService(GraphQLService);
|
const graphqlService = useService(GraphQLService);
|
||||||
const eventSourceService = useService(EventSourceService);
|
const eventSourceService = useService(EventSourceService);
|
||||||
@@ -48,10 +51,16 @@ export const Component = () => {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const framework = useFramework();
|
const framework = useFramework();
|
||||||
const [isBodyProvided, setIsBodyProvided] = useState(false);
|
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 [host, setHost] = useState<EditorHost | null>(null);
|
||||||
const [chatContent, setChatContent] = useState<AIChatContent | 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 chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chatToolContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const widthSignalRef = useRef<Signal<number>>(signal(0));
|
const widthSignalRef = useRef<Signal<number>>(signal(0));
|
||||||
const client = useCopilotClient();
|
const client = useCopilotClient();
|
||||||
|
|
||||||
@@ -64,6 +73,42 @@ export const Component = () => {
|
|||||||
reasoningConfig,
|
reasoningConfig,
|
||||||
} = useAIChatConfig();
|
} = 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
|
// create a temp doc/host for ai-chat-content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let tempDoc: Doc | null = null;
|
let tempDoc: Doc | null = null;
|
||||||
@@ -75,7 +120,6 @@ export const Component = () => {
|
|||||||
store: tempDoc?.getStore() as Store,
|
store: tempDoc?.getStore() as Store,
|
||||||
extensions: getCustomPageEditorBlockSpecs(),
|
extensions: getCustomPageEditorBlockSpecs(),
|
||||||
}).render();
|
}).render();
|
||||||
setDoc(doc);
|
|
||||||
setHost(host);
|
setHost(host);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +130,7 @@ export const Component = () => {
|
|||||||
|
|
||||||
// init or update ai-chat-content
|
// init or update ai-chat-content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isBodyProvided || !host || !doc) {
|
if (!isBodyProvided || !host) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +139,8 @@ export const Component = () => {
|
|||||||
if (!content) {
|
if (!content) {
|
||||||
content = new AIChatContent();
|
content = new AIChatContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
content.session = currentSession;
|
||||||
content.host = host;
|
content.host = host;
|
||||||
content.workspaceId = workspaceId;
|
content.workspaceId = workspaceId;
|
||||||
content.docDisplayConfig = docDisplayConfig;
|
content.docDisplayConfig = docDisplayConfig;
|
||||||
@@ -108,17 +154,6 @@ export const Component = () => {
|
|||||||
|
|
||||||
if (!chatContent) {
|
if (!chatContent) {
|
||||||
// initial values that won't change
|
// 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.createSession = createSession;
|
||||||
content.independentMode = true;
|
content.independentMode = true;
|
||||||
content.onboardingOffsetY = -100;
|
content.onboardingOffsetY = -100;
|
||||||
@@ -128,7 +163,8 @@ export const Component = () => {
|
|||||||
}, [
|
}, [
|
||||||
chatContent,
|
chatContent,
|
||||||
client,
|
client,
|
||||||
doc,
|
createSession,
|
||||||
|
currentSession,
|
||||||
docDisplayConfig,
|
docDisplayConfig,
|
||||||
framework,
|
framework,
|
||||||
host,
|
host,
|
||||||
@@ -139,6 +175,35 @@ export const Component = () => {
|
|||||||
workspaceId,
|
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) => {
|
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
||||||
if (node) {
|
if (node) {
|
||||||
setIsBodyProvided(true);
|
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
|
// observe chat container width and provide to ai-chat-content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isBodyProvided || !chatContainerRef.current) return;
|
if (!isBodyProvided || !chatContainerRef.current) return;
|
||||||
@@ -159,7 +231,12 @@ export const Component = () => {
|
|||||||
<>
|
<>
|
||||||
<ViewTitle title={t['AFFiNE AI']()} />
|
<ViewTitle title={t['AFFiNE AI']()} />
|
||||||
<ViewIcon icon="ai" />
|
<ViewIcon icon="ai" />
|
||||||
<ViewHeader></ViewHeader>
|
<ViewHeader>
|
||||||
|
<div className={styles.chatHeader}>
|
||||||
|
<div />
|
||||||
|
<div ref={onChatToolContainerRef} />
|
||||||
|
</div>
|
||||||
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.chatRoot} ref={onChatContainerRef} />
|
<div className={styles.chatRoot} ref={onChatContainerRef} />
|
||||||
</ViewBody>
|
</ViewBody>
|
||||||
|
|||||||
Reference in New Issue
Block a user