feat: init @affine/copilot (#2511)

This commit is contained in:
Himself65
2023-05-30 18:02:49 +08:00
committed by GitHub
parent f669164674
commit 6648fe4dcc
49 changed files with 2963 additions and 1331 deletions
+33
View File
@@ -0,0 +1,33 @@
import { Button, Input } from '@affine/component';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import { openAIApiKeyAtom } from '../core/hooks';
import { conversationHistoryDBName } from '../core/langchain/message-history';
export const DebugContent: PluginUIAdapter['debugContent'] = () => {
const [key, setKey] = useAtom(openAIApiKeyAtom);
return (
<div>
<span>OpenAI API Key:</span>
<Input
value={key ?? ''}
onChange={useCallback(
(newValue: string) => {
setKey(newValue);
},
[setKey]
)}
/>
<Button
onClick={() => {
indexedDB.deleteDatabase(conversationHistoryDBName);
location.reload();
}}
>
Clean conversations
</Button>
</div>
);
};
+106
View File
@@ -0,0 +1,106 @@
import { Button, Input } from '@affine/component';
import { rootStore } from '@affine/workspace/atom';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { Provider, useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import { Fragment, StrictMode, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Conversation } from '../core/components/conversation';
import { Divider } from '../core/components/divider';
import { openAIApiKeyAtom, useChatAtoms } from '../core/hooks';
if (!environment.isServer) {
import('@blocksuite/blocks').then(({ FormatQuickBar }) => {
FormatQuickBar.customElements.push((_page, getSelection) => {
const div = document.createElement('div');
const root = createRoot(div);
const AskAI = (): ReactElement => {
const { conversationAtom } = useChatAtoms();
const call = useSetAtom(conversationAtom);
return (
<div
onClick={() => {
const selection = getSelection();
if (selection != null) {
const text = selection.models
.map(model => {
return model.text?.toString();
})
.filter((v): v is string => Boolean(v))
.join('\n');
console.log('selected text:', text);
void call(
`I selected some text from the document: \n"${text}."`
);
}
}}
>
Ask AI
</div>
);
};
root.render(
<StrictMode>
<Provider store={rootStore}>
<AskAI />
</Provider>
</StrictMode>
);
return div;
});
});
}
const DetailContentImpl = () => {
const [input, setInput] = useState('');
const { conversationAtom } = useChatAtoms();
const [conversations, call] = useAtom(conversationAtom);
return (
<div
style={{
width: '300px',
}}
>
{conversations.map((message, idx) => {
return (
<Fragment key={idx}>
<Conversation text={message.text} />
<Divider />
</Fragment>
);
})}
<div>
<Input
value={input}
onChange={text => {
setInput(text);
}}
/>
<Button
onClick={() => {
void call(input);
}}
>
send
</Button>
</div>
</div>
);
};
export const DetailContent: PluginUIAdapter['detailContent'] = ({
contentLayoutAtom,
}): ReactElement => {
const layout = useAtomValue(contentLayoutAtom);
const key = useAtomValue(openAIApiKeyAtom);
if (layout === 'editor' || layout.second !== 'com.affine.copilot') {
return <></>;
}
if (!key) {
return <span>Please set OpenAI API Key in the debug panel.</span>;
}
return <DetailContentImpl />;
};
+50
View File
@@ -0,0 +1,50 @@
import { IconButton, Tooltip } from '@affine/component';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import { useCallback } from 'react';
export const HeaderItem: PluginUIAdapter['headerItem'] = ({
contentLayoutAtom,
}): ReactElement => {
const setLayout = useSetAtom(contentLayoutAtom);
return (
<Tooltip content="Chat with AI" placement="bottom-end">
<IconButton
onClick={useCallback(
() =>
setLayout(layout => {
if (layout === 'editor') {
return {
direction: 'row',
first: 'editor',
second: 'com.affine.copilot',
splitPercentage: 80,
};
} else {
return 'editor';
}
}),
[setLayout]
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icon-tabler-brand-hipchat"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M17.802 17.292s.077 -.055 .2 -.149c1.843 -1.425 3 -3.49 3 -5.789c0 -4.286 -4.03 -7.764 -9 -7.764c-4.97 0 -9 3.478 -9 7.764c0 4.288 4.03 7.646 9 7.646c.424 0 1.12 -.028 2.088 -.084c1.262 .82 3.104 1.493 4.716 1.493c.499 0 .734 -.41 .414 -.828c-.486 -.596 -1.156 -1.551 -1.416 -2.29z"></path>
<path d="M7.5 13.5c2.5 2.5 6.5 2.5 9 0"></path>
</svg>
</IconButton>
</Tooltip>
);
};
+12
View File
@@ -0,0 +1,12 @@
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { createElement } from 'react';
import { DebugContent } from './debug-content';
import { DetailContent } from './detail-content';
import { HeaderItem } from './header-item';
export default {
headerItem: props => createElement(HeaderItem, props),
detailContent: props => createElement(DetailContent, props),
debugContent: props => createElement(DebugContent, props),
} satisfies Partial<PluginUIAdapter>;
+3
View File
@@ -0,0 +1,3 @@
import { atom } from 'jotai';
export const contentExpandAtom = atom(false);