mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 10:45:57 +08:00
feat(core): add an independent AI panel (#13004)
close AI-246, AI-285 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Introduced an AI chat interface accessible from the sidebar with a dedicated "/chat" route. * Added "AFFiNE Intelligent" button with AI icon to the sidebar for quick chat access. * Enhanced chat components with an "independent mode" for improved message display and layout. * Improved chat input and content styling, including responsive layout and onboarding offset support. * **Improvements** * Expanded icon support to include an AI icon in the app. * Updated utility and schema functions for greater flexibility and error prevention. * Added a new chat container style for consistent layout and max width. * **Bug Fixes** * Prevented potential errors when certain editor hosts are not provided. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -3,9 +3,9 @@ import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
|
||||
import type { DocSnapshot, Store } from '@blocksuite/affine/store';
|
||||
import { Transformer } from '@blocksuite/affine/store';
|
||||
import { Doc as YDoc } from 'yjs';
|
||||
const getCollection = (() => {
|
||||
export const getCollection = (() => {
|
||||
let collection: WorkspaceImpl | null = null;
|
||||
return async function () {
|
||||
return function () {
|
||||
if (collection) {
|
||||
return collection;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export async function getDocByName(name: DocName) {
|
||||
|
||||
async function initDoc(name: DocName) {
|
||||
const snapshot = (await loaders[name]()) as DocSnapshot;
|
||||
const collection = await getCollection();
|
||||
const collection = getCollection();
|
||||
const transformer = new Transformer({
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
blobCRUD: collection.blobSync,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const chatRoot = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxWidth: 800,
|
||||
padding: '0px 16px',
|
||||
margin: '0 auto',
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import { observeResize } from '@affine/component';
|
||||
import { CopilotClient } from '@affine/core/blocksuite/ai';
|
||||
import { AIChatContent } from '@affine/core/blocksuite/ai/components/ai-chat-content';
|
||||
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';
|
||||
import { getCollection } from '@affine/core/desktop/dialogs/setting/general-setting/editor/edgeless/docs';
|
||||
import {
|
||||
EventSourceService,
|
||||
FetchService,
|
||||
GraphQLService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
ViewBody,
|
||||
ViewHeader,
|
||||
ViewIcon,
|
||||
ViewTitle,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { Doc, Store } from '@blocksuite/affine/store';
|
||||
import { BlockStdScope, type EditorHost } from '@blocksuite/std';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import { useFramework, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
function useCopilotClient() {
|
||||
const graphqlService = useService(GraphQLService);
|
||||
const eventSourceService = useService(EventSourceService);
|
||||
const fetchService = useService(FetchService);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
new CopilotClient(
|
||||
graphqlService.gql,
|
||||
fetchService.fetch,
|
||||
eventSourceService.eventSource
|
||||
),
|
||||
[graphqlService, eventSourceService, fetchService]
|
||||
);
|
||||
}
|
||||
|
||||
export const Component = () => {
|
||||
const t = useI18n();
|
||||
const framework = useFramework();
|
||||
const [isBodyProvided, setIsBodyProvided] = useState(false);
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
const [host, setHost] = useState<EditorHost | null>(null);
|
||||
const [chatContent, setChatContent] = useState<AIChatContent | null>(null);
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const widthSignalRef = useRef<Signal<number>>(signal(0));
|
||||
const client = useCopilotClient();
|
||||
|
||||
const workspaceId = useService(WorkspaceService).workspace.id;
|
||||
|
||||
const {
|
||||
docDisplayConfig,
|
||||
searchMenuConfig,
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
} = useAIChatConfig();
|
||||
|
||||
// create a temp doc/host for ai-chat-content
|
||||
useEffect(() => {
|
||||
let tempDoc: Doc | null = null;
|
||||
const collection = getCollection();
|
||||
const doc = collection.createDoc();
|
||||
tempDoc = doc;
|
||||
doc.load(() => {
|
||||
const host = new BlockStdScope({
|
||||
store: tempDoc?.getStore() as Store,
|
||||
extensions: getCustomPageEditorBlockSpecs(),
|
||||
}).render();
|
||||
setDoc(doc);
|
||||
setHost(host);
|
||||
});
|
||||
|
||||
return () => {
|
||||
tempDoc?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// init or update ai-chat-content
|
||||
useEffect(() => {
|
||||
if (!isBodyProvided || !host || !doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = chatContent;
|
||||
|
||||
if (!content) {
|
||||
content = new AIChatContent();
|
||||
}
|
||||
content.host = host;
|
||||
content.workspaceId = workspaceId;
|
||||
content.docDisplayConfig = docDisplayConfig;
|
||||
content.searchMenuConfig = searchMenuConfig;
|
||||
content.networkSearchConfig = networkSearchConfig;
|
||||
content.reasoningConfig = reasoningConfig;
|
||||
content.affineFeatureFlagService = framework.get(FeatureFlagService);
|
||||
content.affineWorkspaceDialogService = framework.get(
|
||||
WorkspaceDialogService
|
||||
);
|
||||
|
||||
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;
|
||||
chatContainerRef.current?.append(content);
|
||||
setChatContent(content);
|
||||
}
|
||||
}, [
|
||||
chatContent,
|
||||
client,
|
||||
doc,
|
||||
docDisplayConfig,
|
||||
framework,
|
||||
host,
|
||||
isBodyProvided,
|
||||
networkSearchConfig,
|
||||
reasoningConfig,
|
||||
searchMenuConfig,
|
||||
workspaceId,
|
||||
]);
|
||||
|
||||
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
|
||||
if (node) {
|
||||
setIsBodyProvided(true);
|
||||
chatContainerRef.current = node;
|
||||
widthSignalRef.current.value = node.clientWidth;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// observe chat container width and provide to ai-chat-content
|
||||
useEffect(() => {
|
||||
if (!isBodyProvided || !chatContainerRef.current) return;
|
||||
return observeResize(chatContainerRef.current, entry => {
|
||||
widthSignalRef.current.value = entry.contentRect.width;
|
||||
});
|
||||
}, [isBodyProvided]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewTitle title={t['AFFiNE AI']()} />
|
||||
<ViewIcon icon="ai" />
|
||||
<ViewHeader></ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.chatRoot} ref={onChatContainerRef} />
|
||||
</ViewBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
|
||||
export const workbenchRoutes = [
|
||||
{
|
||||
path: '/chat',
|
||||
lazy: () => import('./pages/workspace/chat/index'),
|
||||
},
|
||||
{
|
||||
path: '/all',
|
||||
lazy: () => import('./pages/workspace/all-page/all-page'),
|
||||
|
||||
Reference in New Issue
Block a user