mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 19:15:33 +08:00
feat: init @affine/copilot (#2511)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const contentExpandAtom = atom(false);
|
||||
Reference in New Issue
Block a user