mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat: init new plugin system (#3323)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# `@affine/bookmark-block`
|
||||
|
||||
> Moved to [@affine/bookmark-plugin](../bookmark)
|
||||
>
|
||||
> A block for bookmarking a website
|
||||
|
||||

|
||||
|
||||
@@ -12,18 +12,8 @@
|
||||
"dev": "node ./scripts/dev.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"foxact": "^0.2.11",
|
||||
"link-preview-js": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.47"
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type';
|
||||
import { noop } from 'foxact/noop';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { BookMarkUI } from './ui';
|
||||
|
||||
export default {
|
||||
uiDecorator: editor => {
|
||||
if (
|
||||
editor.parentElement &&
|
||||
editor.page.awarenessStore.getFlag('enable_bookmark_operation')
|
||||
) {
|
||||
const div = document.createElement('div');
|
||||
editor.parentElement.appendChild(div);
|
||||
const root = createRoot(div);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<BookMarkUI page={editor.page} />
|
||||
</StrictMode>
|
||||
);
|
||||
return () => {
|
||||
root.unmount();
|
||||
div.remove();
|
||||
};
|
||||
} else {
|
||||
return noop;
|
||||
}
|
||||
},
|
||||
} satisfies Partial<PluginBlockSuiteAdapter>;
|
||||
@@ -22,26 +22,9 @@ definePlugin(
|
||||
commands: ['com.blocksuite.bookmark-block.get-bookmark-data-by-link'],
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
load: () => import('./blocksuite/index'),
|
||||
hotModuleReload: onHot =>
|
||||
import.meta.webpackHot &&
|
||||
import.meta.webpackHot.accept('./blocksuite', () =>
|
||||
onHot(import('./blocksuite/index'))
|
||||
),
|
||||
},
|
||||
{
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
'./server'
|
||||
),
|
||||
hotModuleReload: onHot =>
|
||||
onHot(
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
'./server'
|
||||
)
|
||||
),
|
||||
load: () => import('./server'),
|
||||
hotModuleReload: onHot => onHot(import('./server')),
|
||||
}
|
||||
);
|
||||
|
||||
16
plugins/bookmark/package.json
Normal file
16
plugins/bookmark/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@affine/bookmark-plugin",
|
||||
"version": "0.1.0",
|
||||
"affinePlugin": {
|
||||
"release": true,
|
||||
"entry": {
|
||||
"core": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@blocksuite/icons": "^2.1.25",
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"foxact": "^0.2.11"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
} from '@blocksuite/blocks/std';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { FC } from 'react';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { StrictMode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export type BookMarkProps = {
|
||||
@@ -95,7 +96,8 @@ const shouldShowBookmarkMenu = (pastedBlocks: SerializedBlock[]) => {
|
||||
}
|
||||
return !!firstBlock.text[0].attributes?.link;
|
||||
};
|
||||
export const BookMarkUI: FC<BookMarkProps> = ({ page }) => {
|
||||
|
||||
const BookMarkUI: FC<BookMarkProps> = ({ page }) => {
|
||||
const [anchor, setAnchor] = useState<Range | null>(null);
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
menuOptions[0].id
|
||||
@@ -218,3 +220,15 @@ export const BookMarkUI: FC<BookMarkProps> = ({ page }) => {
|
||||
</MuiClickAwayListener>
|
||||
) : null;
|
||||
};
|
||||
|
||||
type AppProps = {
|
||||
page: Page;
|
||||
};
|
||||
|
||||
export const App = (props: AppProps): ReactElement => {
|
||||
return (
|
||||
<StrictMode>
|
||||
<BookMarkUI page={props.page} />
|
||||
</StrictMode>
|
||||
);
|
||||
};
|
||||
21
plugins/bookmark/src/index.ts
Normal file
21
plugins/bookmark/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { PluginContext } from '@toeverything/plugin-infra/entry';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { App } from './app';
|
||||
|
||||
export const entry = (context: PluginContext) => {
|
||||
console.log('register');
|
||||
|
||||
context.register('editor', (div, editor) => {
|
||||
const root = createRoot(div);
|
||||
root.render(createElement(App, { page: editor.page }));
|
||||
return () => {
|
||||
root.unmount();
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log('unregister');
|
||||
};
|
||||
};
|
||||
17
plugins/bookmark/tsconfig.json
Normal file
17
plugins/bookmark/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "lib",
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/plugin-infra"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/component"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,28 +1,27 @@
|
||||
{
|
||||
"name": "@affine/copilot",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
"affinePlugin": {
|
||||
"release": false,
|
||||
"entry": {
|
||||
"core": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"idb": "^7.1.1",
|
||||
"langchain": "^0.0.107",
|
||||
"marked": "^5.1.0",
|
||||
"marked-gfm-heading-id": "^3.0.4",
|
||||
"marked-mangle": "^1.1.0"
|
||||
"marked-mangle": "^1.1.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^5.0.0",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"idb": "^7.1.1",
|
||||
"jotai": "^2.2.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"zod": "^3.21.4"
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { SendIcon } from '@blocksuite/icons';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
|
||||
import { Provider, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { contentLayoutAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { StrictMode, Suspense, useCallback, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Suspense, useCallback, useState } from 'react';
|
||||
|
||||
import { ConversationList } from '../core/components/conversation-list';
|
||||
import { FollowingUp } from '../core/components/following-up';
|
||||
@@ -17,51 +15,6 @@ import {
|
||||
textareaStyle,
|
||||
} from './index.css';
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
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);
|
||||
const onClickAskAI = useCallback(() => {
|
||||
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);
|
||||
call(
|
||||
`I selected some text from the document: \n"${text}."`
|
||||
).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}, [call]);
|
||||
|
||||
return <div onClick={onClickAskAI}>Ask AI</div>;
|
||||
};
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Provider store={rootStore}>
|
||||
<AskAI />
|
||||
</Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
return div;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
const Actions = () => {
|
||||
const { conversationAtom, followingUpAtoms } = useChatAtoms();
|
||||
const call = useSetAtom(conversationAtom);
|
||||
@@ -108,12 +61,10 @@ const DetailContentImpl = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailContent: PluginUIAdapter['detailContent'] = ({
|
||||
contentLayoutAtom,
|
||||
}): ReactElement => {
|
||||
export const DetailContent = (): ReactElement => {
|
||||
const layout = useAtomValue(contentLayoutAtom);
|
||||
const key = useAtomValue(openAIApiKeyAtom);
|
||||
if (layout === 'editor' || layout.second !== 'com.affine.copilot') {
|
||||
if (layout === 'editor' || layout.second !== 'copilot') {
|
||||
return <></>;
|
||||
}
|
||||
if (!key) {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { IconButton, Tooltip } from '@affine/component';
|
||||
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
|
||||
import { contentLayoutAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const HeaderItem: PluginUIAdapter['headerItem'] = ({
|
||||
contentLayoutAtom,
|
||||
}): ReactElement => {
|
||||
export const HeaderItem = (): ReactElement => {
|
||||
const setLayout = useSetAtom(contentLayoutAtom);
|
||||
return (
|
||||
<Tooltip content="Chat with AI" placement="bottom-end">
|
||||
@@ -18,7 +16,7 @@ export const HeaderItem: PluginUIAdapter['headerItem'] = ({
|
||||
return {
|
||||
direction: 'horizontal',
|
||||
first: 'editor',
|
||||
second: 'com.affine.copilot',
|
||||
second: 'copilot',
|
||||
splitPercentage: 70,
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { PlusIcon, ResetIcon } from '@blocksuite/icons';
|
||||
import { clsx } from 'clsx';
|
||||
import type { MessageType } from 'langchain/schema';
|
||||
@@ -31,7 +30,6 @@ export const Conversation = (props: ConversationProps): ReactElement => {
|
||||
[styles.avatarRightStyle]: props.type === 'human',
|
||||
})}
|
||||
>
|
||||
<WorkspaceAvatar workspace={null} />
|
||||
<div className={styles.conversationContainerStyle}>
|
||||
<div
|
||||
className={clsx(styles.conversationStyle, {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { IndexedDBChatMessageHistory } from '@affine/copilot/core/langchain/message-history';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { atomWithDefault } from 'jotai/utils';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { atomWithDefault, atomWithStorage } from 'jotai/utils';
|
||||
import type { WritableAtom } from 'jotai/vanilla';
|
||||
import type { LLMChain } from 'langchain/chains';
|
||||
import { type ConversationChain } from 'langchain/chains';
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { definePlugin } from '@toeverything/plugin-infra/manager';
|
||||
import { ReleaseStage } from '@toeverything/plugin-infra/type';
|
||||
import type { PluginContext } from '@toeverything/plugin-infra/entry';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
definePlugin(
|
||||
{
|
||||
id: 'com.affine.copilot',
|
||||
name: {
|
||||
fallback: 'AFFiNE Copilot',
|
||||
i18nKey: 'com.affine.copilot.name',
|
||||
},
|
||||
description: {
|
||||
fallback:
|
||||
'AFFiNE Copilot will help you with best writing experience on the World.',
|
||||
},
|
||||
publisher: {
|
||||
name: {
|
||||
fallback: 'AFFiNE',
|
||||
},
|
||||
link: 'https://affine.pro',
|
||||
},
|
||||
stage: ReleaseStage.NIGHTLY,
|
||||
version: '0.0.1',
|
||||
commands: [],
|
||||
},
|
||||
{
|
||||
load: () => import('./UI/index'),
|
||||
hotModuleReload: onHot =>
|
||||
import.meta.webpackHot &&
|
||||
import.meta.webpackHot.accept('./UI', () => onHot(import('./UI/index'))),
|
||||
}
|
||||
);
|
||||
import { DetailContent } from './UI/detail-content';
|
||||
import { HeaderItem } from './UI/header-item';
|
||||
|
||||
export const entry = (context: PluginContext) => {
|
||||
context.register('headerItem', div => {
|
||||
const root = createRoot(div);
|
||||
root.render(
|
||||
createElement(context.utils.PluginProvider, {}, createElement(HeaderItem))
|
||||
);
|
||||
return () => {
|
||||
root.unmount();
|
||||
};
|
||||
});
|
||||
|
||||
context.register('window', div => {
|
||||
const root = createRoot(div);
|
||||
root.render(
|
||||
createElement(
|
||||
context.utils.PluginProvider,
|
||||
{},
|
||||
createElement(DetailContent)
|
||||
)
|
||||
);
|
||||
return () => {
|
||||
root.unmount();
|
||||
};
|
||||
});
|
||||
return () => {};
|
||||
};
|
||||
|
||||
16
plugins/hello-world/package.json
Normal file
16
plugins/hello-world/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@affine/hello-world-plugin",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"affinePlugin": {
|
||||
"release": false,
|
||||
"entry": {
|
||||
"core": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@blocksuite/icons": "^2.1.25",
|
||||
"@toeverything/plugin-infra": "workspace:*"
|
||||
}
|
||||
}
|
||||
17
plugins/hello-world/src/app.tsx
Normal file
17
plugins/hello-world/src/app.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IconButton, Tooltip } from '@affine/component';
|
||||
import { AffineLogoSBlue2_1Icon } from '@blocksuite/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const HeaderItem = () => {
|
||||
return (
|
||||
<Tooltip content="Plugin Enabled">
|
||||
<IconButton
|
||||
onClick={useCallback(() => {
|
||||
console.log('clicked hello world!');
|
||||
}, [])}
|
||||
>
|
||||
<AffineLogoSBlue2_1Icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
26
plugins/hello-world/src/index.ts
Normal file
26
plugins/hello-world/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { PluginContext } from '@toeverything/plugin-infra/entry';
|
||||
import {
|
||||
currentWorkspaceIdAtom,
|
||||
rootStore,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { HeaderItem } from './app';
|
||||
|
||||
export const entry = (context: PluginContext) => {
|
||||
console.log('register');
|
||||
console.log('hello, world!');
|
||||
console.log(rootStore.get(currentWorkspaceIdAtom));
|
||||
context.register('headerItem', div => {
|
||||
const root = createRoot(div);
|
||||
root.render(createElement(HeaderItem));
|
||||
return () => {
|
||||
root.unmount();
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log('unregister');
|
||||
};
|
||||
};
|
||||
17
plugins/hello-world/tsconfig.json
Normal file
17
plugins/hello-world/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "lib",
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/plugin-infra"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/component"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user