mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
feat: improve copilot (#2758)
This commit is contained in:
@@ -41,6 +41,8 @@ const allPackages = [
|
|||||||
'apps/web',
|
'apps/web',
|
||||||
'apps/server',
|
'apps/server',
|
||||||
'apps/electron',
|
'apps/electron',
|
||||||
|
'plugins/copilot',
|
||||||
|
'plugins/bookmark-block',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { globalStyle } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
globalStyle('.mosaic.mosaic-blueprint-theme', {
|
export const pluginContainer = style({
|
||||||
backgroundColor: 'var(--background-color)',
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { pageSettingFamily } from '../atoms';
|
|||||||
import { contentLayoutAtom } from '../atoms/layout';
|
import { contentLayoutAtom } from '../atoms/layout';
|
||||||
import type { AffineOfficialWorkspace } from '../shared';
|
import type { AffineOfficialWorkspace } from '../shared';
|
||||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||||
|
import { pluginContainer } from './page-detail-editor.css';
|
||||||
|
|
||||||
export type PageDetailEditorProps = {
|
export type PageDetailEditorProps = {
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
@@ -122,7 +123,7 @@ const PluginContentAdapter = memo<{
|
|||||||
detailContent: PluginUIAdapter['detailContent'];
|
detailContent: PluginUIAdapter['detailContent'];
|
||||||
}>(function PluginContentAdapter({ detailContent }) {
|
}>(function PluginContentAdapter({ detailContent }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={pluginContainer}>
|
||||||
{detailContent({
|
{detailContent({
|
||||||
contentLayoutAtom,
|
contentLayoutAtom,
|
||||||
})}
|
})}
|
||||||
@@ -153,7 +154,12 @@ const LayoutPanel = memo(function LayoutPanel(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<PanelGroup direction={node.direction}>
|
<PanelGroup
|
||||||
|
style={{
|
||||||
|
height: 'calc(100% - 52px)',
|
||||||
|
}}
|
||||||
|
direction={node.direction}
|
||||||
|
>
|
||||||
<Panel defaultSize={node.splitPercentage}>
|
<Panel defaultSize={node.splitPercentage}>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<LayoutPanel
|
<LayoutPanel
|
||||||
|
|||||||
+4
-1
@@ -102,5 +102,8 @@
|
|||||||
"@types/cacheable-request": "^6.0.2",
|
"@types/cacheable-request": "^6.0.2",
|
||||||
"@types/responselike": "^1.0.0"
|
"@types/responselike": "^1.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.6.0"
|
"packageManager": "yarn@3.6.0",
|
||||||
|
"dependencies": {
|
||||||
|
"marked-mangle": "^1.0.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@affine/component": "workspace:*",
|
"@affine/component": "workspace:*",
|
||||||
"@toeverything/plugin-infra": "workspace:*"
|
"@toeverything/plugin-infra": "workspace:*",
|
||||||
|
"langchain": "^0.0.92",
|
||||||
|
"marked": "^5.0.4",
|
||||||
|
"marked-gfm-heading-id": "^3.0.4",
|
||||||
|
"marked-mangle": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/marked": "^5.0.0",
|
"@types/marked": "^5.0.0",
|
||||||
@@ -16,10 +20,9 @@
|
|||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"jotai": "^2.1.1",
|
"jotai": "^2.1.1",
|
||||||
"langchain": "^0.0.90",
|
|
||||||
"marked": "^5.0.4",
|
|
||||||
"react": "18.3.0-canary-16d053d59-20230506",
|
"react": "18.3.0-canary-16d053d59-20230506",
|
||||||
"react-dom": "18.3.0-canary-16d053d59-20230506"
|
"react-dom": "18.3.0-canary-16d053d59-20230506",
|
||||||
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Button, Input } from '@affine/component';
|
import { Button, Input } from '@affine/component';
|
||||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||||
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
|
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
|
||||||
import { Provider, useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { Provider, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { Fragment, StrictMode, useState } from 'react';
|
import { StrictMode, Suspense, useCallback, useState } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { Conversation } from '../core/components/conversation';
|
import { ConversationList } from '../core/components/conversation-list';
|
||||||
import { Divider } from '../core/components/divider';
|
import { FollowingUp } from '../core/components/following-up';
|
||||||
import { openAIApiKeyAtom, useChatAtoms } from '../core/hooks';
|
import { openAIApiKeyAtom, useChatAtoms } from '../core/hooks';
|
||||||
|
import { detailContentActionsStyle, detailContentStyle } from './index.css';
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
import('@blocksuite/blocks').then(({ FormatQuickBar }) => {
|
import('@blocksuite/blocks').then(({ FormatQuickBar }) => {
|
||||||
@@ -54,25 +55,16 @@ if (typeof window === 'undefined') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const DetailContentImpl = () => {
|
const Actions = () => {
|
||||||
|
const { conversationAtom, followingUpAtoms } = useChatAtoms();
|
||||||
|
const call = useSetAtom(conversationAtom);
|
||||||
|
const questions = useAtomValue(followingUpAtoms.questionsAtom);
|
||||||
|
const generateFollowingUp = useSetAtom(followingUpAtoms.generateChatAtom);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const { conversationAtom } = useChatAtoms();
|
|
||||||
const [conversations, call] = useAtom(conversationAtom);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
style={{
|
<FollowingUp questions={questions} />
|
||||||
width: '300px',
|
<div className={detailContentActionsStyle}>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{conversations.map((message, idx) => {
|
|
||||||
return (
|
|
||||||
<Fragment key={idx}>
|
|
||||||
<Conversation text={message.text} />
|
|
||||||
<Divider />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div>
|
|
||||||
<Input
|
<Input
|
||||||
value={input}
|
value={input}
|
||||||
onChange={text => {
|
onChange={text => {
|
||||||
@@ -80,13 +72,28 @@ const DetailContentImpl = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={useCallback(async () => {
|
||||||
void call(input);
|
await call(input);
|
||||||
}}
|
await generateFollowingUp();
|
||||||
|
}, [call, generateFollowingUp, input])}
|
||||||
>
|
>
|
||||||
send
|
send
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailContentImpl = () => {
|
||||||
|
const { conversationAtom } = useChatAtoms();
|
||||||
|
const conversations = useAtomValue(conversationAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={detailContentStyle}>
|
||||||
|
<ConversationList conversations={conversations} />
|
||||||
|
<Suspense fallback="generating follow-up question">
|
||||||
|
<Actions />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const HeaderItem: PluginUIAdapter['headerItem'] = ({
|
|||||||
direction: 'horizontal',
|
direction: 'horizontal',
|
||||||
first: 'editor',
|
first: 'editor',
|
||||||
second: 'com.affine.copilot',
|
second: 'com.affine.copilot',
|
||||||
splitPercentage: 80,
|
splitPercentage: 70,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return 'editor';
|
return 'editor';
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const detailContentStyle = style({
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
|
||||||
|
overflow: 'auto',
|
||||||
|
|
||||||
|
paddingLeft: '9px',
|
||||||
|
paddingRight: '9px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const detailContentActionsStyle = style({});
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { ConversationChain } from 'langchain/chains';
|
import { ConversationChain, LLMChain } from 'langchain/chains';
|
||||||
import { ChatOpenAI } from 'langchain/chat_models/openai';
|
import { ChatOpenAI } from 'langchain/chat_models/openai';
|
||||||
import { BufferMemory } from 'langchain/memory';
|
import { BufferMemory } from 'langchain/memory';
|
||||||
import {
|
import {
|
||||||
ChatPromptTemplate,
|
ChatPromptTemplate,
|
||||||
HumanMessagePromptTemplate,
|
HumanMessagePromptTemplate,
|
||||||
MessagesPlaceholder,
|
MessagesPlaceholder,
|
||||||
|
PromptTemplate,
|
||||||
SystemMessagePromptTemplate,
|
SystemMessagePromptTemplate,
|
||||||
} from 'langchain/prompts';
|
} from 'langchain/prompts';
|
||||||
import { type LLMResult } from 'langchain/schema';
|
import { type LLMResult } from 'langchain/schema';
|
||||||
|
|
||||||
import { IndexedDBChatMessageHistory } from './langchain/message-history';
|
import { IndexedDBChatMessageHistory } from './langchain/message-history';
|
||||||
import { chatPrompt } from './prompts';
|
import { chatPrompt, followupQuestionPrompt } from './prompts';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface WindowEventMap {
|
interface WindowEventMap {
|
||||||
@@ -22,13 +23,24 @@ declare global {
|
|||||||
export async function createChatAI(
|
export async function createChatAI(
|
||||||
room: string,
|
room: string,
|
||||||
openAIApiKey: string
|
openAIApiKey: string
|
||||||
): Promise<ConversationChain> {
|
): Promise<{
|
||||||
|
conversationChain: ConversationChain;
|
||||||
|
followupChain: LLMChain<string>;
|
||||||
|
chatHistory: IndexedDBChatMessageHistory;
|
||||||
|
}> {
|
||||||
if (!openAIApiKey) {
|
if (!openAIApiKey) {
|
||||||
console.warn('OpenAI API key not set, chat will not work');
|
console.warn('OpenAI API key not set, chat will not work');
|
||||||
}
|
}
|
||||||
|
const followup = new ChatOpenAI({
|
||||||
|
streaming: false,
|
||||||
|
modelName: 'gpt-3.5-turbo',
|
||||||
|
temperature: 0.5,
|
||||||
|
openAIApiKey: openAIApiKey,
|
||||||
|
});
|
||||||
|
|
||||||
const chat = new ChatOpenAI({
|
const chat = new ChatOpenAI({
|
||||||
streaming: true,
|
streaming: true,
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-3.5-turbo',
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
openAIApiKey: openAIApiKey,
|
openAIApiKey: openAIApiKey,
|
||||||
callbacks: [
|
callbacks: [
|
||||||
@@ -77,13 +89,32 @@ export async function createChatAI(
|
|||||||
HumanMessagePromptTemplate.fromTemplate('{input}'),
|
HumanMessagePromptTemplate.fromTemplate('{input}'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new ConversationChain({
|
const followupPromptTemplate = new PromptTemplate({
|
||||||
|
template: followupQuestionPrompt,
|
||||||
|
inputVariables: ['human_conversation', 'ai_conversation'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const followupChain = new LLMChain({
|
||||||
|
llm: followup,
|
||||||
|
prompt: followupPromptTemplate,
|
||||||
|
memory: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatHistory = new IndexedDBChatMessageHistory(room);
|
||||||
|
|
||||||
|
const conversationChain = new ConversationChain({
|
||||||
memory: new BufferMemory({
|
memory: new BufferMemory({
|
||||||
returnMessages: true,
|
returnMessages: true,
|
||||||
memoryKey: 'history',
|
memoryKey: 'history',
|
||||||
chatHistory: new IndexedDBChatMessageHistory(room),
|
chatHistory,
|
||||||
}),
|
}),
|
||||||
prompt: chatPromptTemplate,
|
prompt: chatPromptTemplate,
|
||||||
llm: chat,
|
llm: chat,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
conversationChain,
|
||||||
|
followupChain,
|
||||||
|
chatHistory,
|
||||||
|
} as const;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const conversationListStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '30px',
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { BaseChatMessage } from 'langchain/schema';
|
||||||
|
|
||||||
|
import { Conversation } from '../conversation';
|
||||||
|
import { conversationListStyle } from './index.css';
|
||||||
|
|
||||||
|
export type ConversationListProps = {
|
||||||
|
conversations: BaseChatMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConversationList = (props: ConversationListProps) => {
|
||||||
|
return (
|
||||||
|
<div className={conversationListStyle}>
|
||||||
|
{props.conversations.map((conversation, idx) => (
|
||||||
|
<Conversation
|
||||||
|
type={conversation._getType()}
|
||||||
|
text={conversation.text}
|
||||||
|
key={idx}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { marked } from 'marked';
|
|
||||||
import { type ReactElement, useMemo } from 'react';
|
|
||||||
|
|
||||||
export interface ConversationProps {
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Conversation = (props: ConversationProps): ReactElement => {
|
|
||||||
const html = useMemo(() => marked.parse(props.text), [props.text]);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: html,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const conversationStyle = style({
|
||||||
|
padding: '10px 18px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const aiMessageStyle = style({
|
||||||
|
backgroundColor: 'rgba(207, 252, 255, 0.3)',
|
||||||
|
borderRadius: '18px 18px 18px 2px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const humanMessageStyle = style({
|
||||||
|
borderRadius: '18px 18px 2px 18px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { clsx } from 'clsx';
|
||||||
|
import type { MessageType } from 'langchain/schema';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import { gfmHeadingId } from 'marked-gfm-heading-id';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
import { mangle } from 'marked-mangle';
|
||||||
|
import { type ReactElement, useMemo } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
aiMessageStyle,
|
||||||
|
conversationStyle,
|
||||||
|
humanMessageStyle,
|
||||||
|
} from './index.css';
|
||||||
|
|
||||||
|
marked.use(
|
||||||
|
gfmHeadingId({
|
||||||
|
prefix: 'affine-',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
marked.use(mangle());
|
||||||
|
|
||||||
|
export interface ConversationProps {
|
||||||
|
type: MessageType;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Conversation = (props: ConversationProps): ReactElement => {
|
||||||
|
const html = useMemo(() => marked.parse(props.text), [props.text]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(conversationStyle, {
|
||||||
|
[aiMessageStyle]: props.type === 'ai',
|
||||||
|
[humanMessageStyle]: props.type === 'human',
|
||||||
|
})}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: html,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const followingUpStyle = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '10px',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const questionStyle = style({
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#8E8D91',
|
||||||
|
padding: '2px 10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { ReactElement } from 'react';
|
||||||
|
|
||||||
|
import { followingUpStyle, questionStyle } from './index.css';
|
||||||
|
|
||||||
|
export type FollowingUpProps = {
|
||||||
|
questions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FollowingUp = (props: FollowingUpProps): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className={followingUpStyle}>
|
||||||
|
{props.questions.map((question, index) => (
|
||||||
|
<div className={questionStyle} key={index}>
|
||||||
|
{question}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import type { IndexedDBChatMessageHistory } from '@affine/copilot/core/langchain/message-history';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import { atomFamily } from 'jotai/utils';
|
import { atomWithDefault } from 'jotai/utils';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
import type { WritableAtom } from 'jotai/vanilla';
|
||||||
|
import type { LLMChain } from 'langchain/chains';
|
||||||
import { type ConversationChain } from 'langchain/chains';
|
import { type ConversationChain } from 'langchain/chains';
|
||||||
import { type BufferMemory } from 'langchain/memory';
|
import { type BufferMemory } from 'langchain/memory';
|
||||||
import {
|
import {
|
||||||
@@ -8,9 +11,12 @@ import {
|
|||||||
type BaseChatMessage,
|
type BaseChatMessage,
|
||||||
HumanChatMessage,
|
HumanChatMessage,
|
||||||
} from 'langchain/schema';
|
} from 'langchain/schema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { createChatAI } from '../chat';
|
import { createChatAI } from '../chat';
|
||||||
|
|
||||||
|
const followupResponseSchema = z.array(z.string());
|
||||||
|
|
||||||
export const openAIApiKeyAtom = atomWithStorage<string | null>(
|
export const openAIApiKeyAtom = atomWithStorage<string | null>(
|
||||||
'com.affine.copilot.openai.token',
|
'com.affine.copilot.openai.token',
|
||||||
null
|
null
|
||||||
@@ -19,12 +25,24 @@ export const openAIApiKeyAtom = atomWithStorage<string | null>(
|
|||||||
export const chatAtom = atom(async get => {
|
export const chatAtom = atom(async get => {
|
||||||
const openAIApiKey = get(openAIApiKeyAtom);
|
const openAIApiKey = get(openAIApiKeyAtom);
|
||||||
if (!openAIApiKey) {
|
if (!openAIApiKey) {
|
||||||
return null;
|
throw new Error('OpenAI API key not set, chat will not work');
|
||||||
}
|
}
|
||||||
return createChatAI('default-copilot', openAIApiKey);
|
return createChatAI('default-copilot', openAIApiKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversationAtomFamily = atomFamily((chat: ConversationChain | null) => {
|
const conversationWeakMap = new WeakMap<
|
||||||
|
ConversationChain,
|
||||||
|
WritableAtom<BaseChatMessage[], [string], Promise<void>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
const getConversationAtom = (chat: ConversationChain) => {
|
||||||
|
if (conversationWeakMap.has(chat)) {
|
||||||
|
return conversationWeakMap.get(chat) as WritableAtom<
|
||||||
|
BaseChatMessage[],
|
||||||
|
[string],
|
||||||
|
Promise<void>
|
||||||
|
>;
|
||||||
|
}
|
||||||
const conversationBaseAtom = atom<BaseChatMessage[]>([]);
|
const conversationBaseAtom = atom<BaseChatMessage[]>([]);
|
||||||
conversationBaseAtom.onMount = setAtom => {
|
conversationBaseAtom.onMount = setAtom => {
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
@@ -52,7 +70,7 @@ const conversationAtomFamily = atomFamily((chat: ConversationChain | null) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return atom<BaseChatMessage[], [string], Promise<void>>(
|
const conversationAtom = atom<BaseChatMessage[], [string], Promise<void>>(
|
||||||
get => get(conversationBaseAtom),
|
get => get(conversationBaseAtom),
|
||||||
async (get, set, input) => {
|
async (get, set, input) => {
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
@@ -73,14 +91,75 @@ const conversationAtomFamily = atomFamily((chat: ConversationChain | null) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
conversationWeakMap.set(chat, conversationAtom);
|
||||||
|
return conversationAtom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const followingUpWeakMap = new WeakMap<
|
||||||
|
LLMChain<string>,
|
||||||
|
{
|
||||||
|
questionsAtom: ReturnType<typeof atomWithDefault<Promise<string[]>>>;
|
||||||
|
generateChatAtom: WritableAtom<null, [], void>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
const getFollowingUpAtoms = (
|
||||||
|
followupLLMChain: LLMChain<string>,
|
||||||
|
chatHistory: IndexedDBChatMessageHistory
|
||||||
|
) => {
|
||||||
|
if (followingUpWeakMap.has(followupLLMChain)) {
|
||||||
|
return followingUpWeakMap.get(followupLLMChain) as {
|
||||||
|
questionsAtom: ReturnType<typeof atomWithDefault<Promise<string[]>>>;
|
||||||
|
generateChatAtom: WritableAtom<null, [], void>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const baseAtom = atomWithDefault<Promise<string[]>>(async () => {
|
||||||
|
return chatHistory?.getFollowingUp() ?? [];
|
||||||
|
});
|
||||||
|
const setAtom = atom<null, [], void>(null, async (get, set) => {
|
||||||
|
if (!followupLLMChain || !chatHistory) {
|
||||||
|
throw new Error('followupLLMChain not set');
|
||||||
|
}
|
||||||
|
const messages = await chatHistory.getMessages();
|
||||||
|
const aiMessage = messages.findLast(
|
||||||
|
message => message._getType() === 'ai'
|
||||||
|
)?.text;
|
||||||
|
const humanMessage = messages.findLast(
|
||||||
|
message => message._getType() === 'human'
|
||||||
|
)?.text;
|
||||||
|
const response = await followupLLMChain.call({
|
||||||
|
ai_conversation: aiMessage,
|
||||||
|
human_conversation: humanMessage,
|
||||||
|
});
|
||||||
|
const followingUp = JSON.parse(response.text);
|
||||||
|
followupResponseSchema.parse(followingUp);
|
||||||
|
set(baseAtom, followingUp);
|
||||||
|
chatHistory.saveFollowingUp(followingUp).catch(() => {
|
||||||
|
console.error('failed to save followup');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
followingUpWeakMap.set(followupLLMChain, {
|
||||||
|
questionsAtom: baseAtom,
|
||||||
|
generateChatAtom: setAtom,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
questionsAtom: baseAtom,
|
||||||
|
generateChatAtom: setAtom,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function useChatAtoms(): {
|
export function useChatAtoms(): {
|
||||||
conversationAtom: ReturnType<typeof conversationAtomFamily>;
|
conversationAtom: ReturnType<typeof getConversationAtom>;
|
||||||
|
followingUpAtoms: ReturnType<typeof getFollowingUpAtoms>;
|
||||||
} {
|
} {
|
||||||
const chat = useAtomValue(chatAtom);
|
const chat = useAtomValue(chatAtom);
|
||||||
const conversationAtom = conversationAtomFamily(chat);
|
const conversationAtom = getConversationAtom(chat.conversationChain);
|
||||||
|
const followingUpAtoms = getFollowingUpAtoms(
|
||||||
|
chat.followupChain,
|
||||||
|
chat.chatHistory
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
conversationAtom,
|
conversationAtom,
|
||||||
|
followingUpAtoms,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,25 +23,42 @@ interface ChatMessageDBV1 extends DBSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChatMessageDBV2 extends ChatMessageDBV1 {
|
||||||
|
followingUp: {
|
||||||
|
key: string;
|
||||||
|
value: {
|
||||||
|
/**
|
||||||
|
* ID of the chat
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
question: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const conversationHistoryDBName = 'affine-copilot-chat';
|
export const conversationHistoryDBName = 'affine-copilot-chat';
|
||||||
|
|
||||||
export class IndexedDBChatMessageHistory extends BaseChatMessageHistory {
|
export class IndexedDBChatMessageHistory extends BaseChatMessageHistory {
|
||||||
public id: string;
|
public id: string;
|
||||||
private messages: BaseChatMessage[] = [];
|
private messages: BaseChatMessage[] = [];
|
||||||
|
|
||||||
private readonly dbPromise: Promise<IDBPDatabase<ChatMessageDBV1>>;
|
private readonly dbPromise: Promise<IDBPDatabase<ChatMessageDBV2>>;
|
||||||
private readonly initPromise: Promise<void>;
|
private readonly initPromise: Promise<void>;
|
||||||
|
|
||||||
constructor(id: string) {
|
constructor(id: string) {
|
||||||
super();
|
super();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.dbPromise = openDB<ChatMessageDBV1>('affine-copilot-chat', 1, {
|
this.dbPromise = openDB<ChatMessageDBV2>('affine-copilot-chat', 2, {
|
||||||
upgrade(database, oldVersion) {
|
upgrade(database, oldVersion) {
|
||||||
if (oldVersion === 0) {
|
if (oldVersion === 0) {
|
||||||
database.createObjectStore('chat', {
|
database.createObjectStore('chat', {
|
||||||
keyPath: 'id',
|
keyPath: 'id',
|
||||||
});
|
});
|
||||||
|
} else if (oldVersion === 1) {
|
||||||
|
database.createObjectStore('followingUp', {
|
||||||
|
keyPath: 'id',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -70,6 +87,31 @@ export class IndexedDBChatMessageHistory extends BaseChatMessageHistory {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async saveFollowingUp(question: string[]): Promise<void> {
|
||||||
|
await this.initPromise;
|
||||||
|
const db = await this.dbPromise;
|
||||||
|
const t = db
|
||||||
|
.transaction('followingUp', 'readwrite')
|
||||||
|
.objectStore('followingUp');
|
||||||
|
await t.put({
|
||||||
|
id: this.id,
|
||||||
|
question,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFollowingUp(): Promise<string[]> {
|
||||||
|
await this.initPromise;
|
||||||
|
const db = await this.dbPromise;
|
||||||
|
const t = db
|
||||||
|
.transaction('followingUp', 'readonly')
|
||||||
|
.objectStore('followingUp');
|
||||||
|
const chat = await t.get(this.id);
|
||||||
|
if (chat != null) {
|
||||||
|
return chat.question;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
protected async addMessage(message: BaseChatMessage): Promise<void> {
|
protected async addMessage(message: BaseChatMessage): Promise<void> {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
@@ -104,6 +146,6 @@ export class IndexedDBChatMessageHistory extends BaseChatMessageHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMessages(): Promise<BaseChatMessage[]> {
|
async getMessages(): Promise<BaseChatMessage[]> {
|
||||||
return await this.initPromise.then(() => this.messages);
|
return this.initPromise.then(() => this.messages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,19 @@ Keep your answers short and impersonal.
|
|||||||
The user works in an app called AFFiNE, which has a concept for an editor, a page for a single document, workspace for a collection of documents.
|
The user works in an app called AFFiNE, which has a concept for an editor, a page for a single document, workspace for a collection of documents.
|
||||||
The active document is the markdown file the user is looking at.
|
The active document is the markdown file the user is looking at.
|
||||||
Use Markdown formatting in your answers.
|
Use Markdown formatting in your answers.
|
||||||
Wrap your answers into triple backticks.
|
|
||||||
You can only give one reply for each conversation turn.
|
You can only give one reply for each conversation turn.
|
||||||
You should always generate short suggestions for the next user turns that are relevant to the conversation and not offensive.
|
`;
|
||||||
You should reply to the users within 150 characters.
|
|
||||||
|
export const followupQuestionPrompt = `Rules you must follow:
|
||||||
|
- You only respond in JSON format
|
||||||
|
- Read the following conversation between AI and Human and generate at most 3 follow-up messages or questions the Human can ask
|
||||||
|
- Your response MUST be a valid JSON array of strings like this: ["some question", "another question"]
|
||||||
|
- Each message in your response should be concise, no more than 15 words
|
||||||
|
- You MUST reply in the same written language as the conversation
|
||||||
|
- Don't output anything other text
|
||||||
|
The conversation is inside triple quotes:
|
||||||
|
\`\`\`
|
||||||
|
Human: {human_conversation}
|
||||||
|
AI: {ai_conversation}
|
||||||
|
\`\`\`
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -130,10 +130,13 @@ __metadata:
|
|||||||
"@types/react-dom": ^18.2.4
|
"@types/react-dom": ^18.2.4
|
||||||
idb: ^7.1.1
|
idb: ^7.1.1
|
||||||
jotai: ^2.1.1
|
jotai: ^2.1.1
|
||||||
langchain: ^0.0.90
|
langchain: ^0.0.92
|
||||||
marked: ^5.0.4
|
marked: ^5.0.4
|
||||||
|
marked-gfm-heading-id: ^3.0.4
|
||||||
|
marked-mangle: ^1.0.1
|
||||||
react: 18.3.0-canary-16d053d59-20230506
|
react: 18.3.0-canary-16d053d59-20230506
|
||||||
react-dom: 18.3.0-canary-16d053d59-20230506
|
react-dom: 18.3.0-canary-16d053d59-20230506
|
||||||
|
zod: ^3.21.4
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: "*"
|
react: "*"
|
||||||
react-dom: "*"
|
react-dom: "*"
|
||||||
@@ -300,6 +303,7 @@ __metadata:
|
|||||||
husky: ^8.0.3
|
husky: ^8.0.3
|
||||||
lint-staged: ^13.2.2
|
lint-staged: ^13.2.2
|
||||||
madge: ^6.1.0
|
madge: ^6.1.0
|
||||||
|
marked-mangle: ^1.0.1
|
||||||
msw: ^1.2.1
|
msw: ^1.2.1
|
||||||
nanoid: ^4.0.2
|
nanoid: ^4.0.2
|
||||||
nx: 16.3.2
|
nx: 16.3.2
|
||||||
@@ -16830,6 +16834,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"github-slugger@npm:^2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "github-slugger@npm:2.0.0"
|
||||||
|
checksum: 250375cde2058f21454872c2c79f72c4637340c30c51ff158ca4ec71cbc478f33d54477d787a662f9207aeb095a2060f155bc01f15329ba8a5fb6698e0fc81f8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
|
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
resolution: "glob-parent@npm:5.1.2"
|
resolution: "glob-parent@npm:5.1.2"
|
||||||
@@ -19787,9 +19798,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"langchain@npm:^0.0.90":
|
"langchain@npm:^0.0.92":
|
||||||
version: 0.0.90
|
version: 0.0.92
|
||||||
resolution: "langchain@npm:0.0.90"
|
resolution: "langchain@npm:0.0.92"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@anthropic-ai/sdk": ^0.4.3
|
"@anthropic-ai/sdk": ^0.4.3
|
||||||
ansi-styles: ^5.0.0
|
ansi-styles: ^5.0.0
|
||||||
@@ -19835,7 +19846,7 @@ __metadata:
|
|||||||
cohere-ai: ^5.0.2
|
cohere-ai: ^5.0.2
|
||||||
d3-dsv: ^2.0.0
|
d3-dsv: ^2.0.0
|
||||||
epub2: ^3.0.1
|
epub2: ^3.0.1
|
||||||
faiss-node: ^0.2.0
|
faiss-node: ^0.2.1
|
||||||
google-auth-library: ^8.8.0
|
google-auth-library: ^8.8.0
|
||||||
hnswlib-node: ^1.4.2
|
hnswlib-node: ^1.4.2
|
||||||
html-to-text: ^9.0.5
|
html-to-text: ^9.0.5
|
||||||
@@ -19947,7 +19958,7 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
weaviate-ts-client:
|
weaviate-ts-client:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: cbd35a7397b30ad265bf4ce992818ee03a409c33dde7a7a1e17c76e7e875768dbc79ff06618e42b3c688081ff8b0ff5eb34b532a2994d078578de67937a6452b
|
checksum: a9fb6cdf48e971f9327bcf5eb2322eb9b9add63413ecacd90bb064feab40d3ba6024086ccade59c0285dd57249406dda9ba7847cdb02e08312e147618165b49e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -20760,6 +20771,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"marked-gfm-heading-id@npm:^3.0.4":
|
||||||
|
version: 3.0.4
|
||||||
|
resolution: "marked-gfm-heading-id@npm:3.0.4"
|
||||||
|
dependencies:
|
||||||
|
github-slugger: ^2.0.0
|
||||||
|
peerDependencies:
|
||||||
|
marked: ^4 || ^5
|
||||||
|
checksum: 8c65d0fe0f59291d6f4529f49a4af00cad5787a72a59b9397f82d6ab58b1affbf53678f7efdca77fa75c32e80bbdab1c53c2aa5d7ece3e821d2fe35a76fb4930
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"marked-mangle@npm:^1.0.1":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "marked-mangle@npm:1.0.1"
|
||||||
|
peerDependencies:
|
||||||
|
marked: ^4 || ^5
|
||||||
|
checksum: 7ebdd69eb41907a865141e7cce1cd8c5c24dd6955c63aaf5299710aba2935abdfbca1b1ea0ff30cf527ff20a940c3b5a9311fca9abb84f4d294767b0435213fe
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"marked@npm:^4.2.12":
|
"marked@npm:^4.2.12":
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
resolution: "marked@npm:4.3.0"
|
resolution: "marked@npm:4.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user