mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-22 09:13:18 +03:00
feat: init new plugin system (#3323)
This commit is contained in:
parent
604b53d9a4
commit
19055baa49
@ -12,3 +12,5 @@ tests/affine-legacy/0.7.0-canary.18/static
|
||||
.github/helm
|
||||
_next
|
||||
storybook-static
|
||||
web-static
|
||||
public
|
||||
|
@ -276,12 +276,13 @@ export const createConfiguration: (
|
||||
|
||||
devServer: {
|
||||
hot: 'only',
|
||||
liveReload: false,
|
||||
liveReload: true,
|
||||
client: undefined,
|
||||
historyApiFallback: true,
|
||||
static: {
|
||||
directory: resolve(rootPath, 'public'),
|
||||
publicPath: '/',
|
||||
watch: true,
|
||||
},
|
||||
} as DevServerConfiguration,
|
||||
} satisfies webpack.Configuration;
|
||||
|
@ -38,6 +38,7 @@
|
||||
rel="shortcut icon"
|
||||
href="https://affine.pro/favicon.ico"
|
||||
/>
|
||||
<link rel="stylesheet" href="/plugins/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
@ -10,7 +10,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/bookmark-block": "workspace:*",
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/copilot": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
@ -53,6 +52,7 @@
|
||||
"react-resizable-panels": "^0.0.53",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"ses": "^0.18.5",
|
||||
"swr": "^2.1.5",
|
||||
"y-protocols": "^1.0.5",
|
||||
"yjs": "^13.6.6",
|
||||
|
@ -45,7 +45,7 @@
|
||||
"options": {
|
||||
"script": "build"
|
||||
},
|
||||
"outputs": ["{projectRoot}/dist"]
|
||||
"outputs": ["{projectRoot}/dist", "{projectRoot}/public/plugins"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
apps/core/public/.gitignore
vendored
4
apps/core/public/.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
*.js
|
||||
*.map
|
||||
*.map
|
||||
|
||||
plugins
|
||||
|
@ -48,8 +48,6 @@ currentWorkspaceIdAtom.onMount = set => {
|
||||
if (value) {
|
||||
set(value);
|
||||
localStorage.setItem('last_workspace_id', value);
|
||||
} else {
|
||||
set(null);
|
||||
}
|
||||
};
|
||||
callback(router.state);
|
||||
@ -65,8 +63,6 @@ currentPageIdAtom.onMount = set => {
|
||||
const value = state.location.pathname.split('/')[3];
|
||||
if (value) {
|
||||
set(value);
|
||||
} else {
|
||||
set(null);
|
||||
}
|
||||
};
|
||||
callback(router.state);
|
||||
|
@ -1,34 +0,0 @@
|
||||
import type { ExpectedLayout } from '@toeverything/plugin-infra/type';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
|
||||
|
||||
type SetStateAction<Value> = Value | ((prev: Value) => Value);
|
||||
export const contentLayoutAtom = atom<
|
||||
ExpectedLayout,
|
||||
[SetStateAction<ExpectedLayout>],
|
||||
void
|
||||
>(
|
||||
get => get(contentLayoutBaseAtom),
|
||||
(get, set, layout) => {
|
||||
set(contentLayoutBaseAtom, prev => {
|
||||
let setV: (prev: ExpectedLayout) => ExpectedLayout;
|
||||
if (typeof layout !== 'function') {
|
||||
setV = () => layout;
|
||||
} else {
|
||||
setV = layout;
|
||||
}
|
||||
const nextValue = setV(prev);
|
||||
if (nextValue === 'editor') {
|
||||
return nextValue;
|
||||
}
|
||||
if (nextValue.first !== 'editor') {
|
||||
throw new Error('The first element of the layout should be editor.');
|
||||
}
|
||||
if (nextValue.splitPercentage && nextValue.splitPercentage < 70) {
|
||||
throw new Error('The split percentage should be greater than 70.');
|
||||
}
|
||||
return nextValue;
|
||||
});
|
||||
}
|
||||
);
|
@ -1,6 +1,181 @@
|
||||
import('@affine/bookmark-block');
|
||||
if (runtimeConfig.enablePlugin) {
|
||||
import('@affine/copilot');
|
||||
}
|
||||
/// <reference types="@types/webpack-env" />
|
||||
import 'ses';
|
||||
|
||||
import * as AFFiNEComponent from '@affine/component';
|
||||
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
|
||||
import * as Icons from '@blocksuite/icons';
|
||||
import type {
|
||||
CallbackMap,
|
||||
PluginContext,
|
||||
} from '@toeverything/plugin-infra/entry';
|
||||
import * as Manager from '@toeverything/plugin-infra/manager';
|
||||
import {
|
||||
editorItemsAtom,
|
||||
headerItemsAtom,
|
||||
registeredPluginAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import * as Jotai from 'jotai';
|
||||
import { Provider } from 'jotai/react';
|
||||
import * as JotaiUtils from 'jotai/utils';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import * as React from 'react';
|
||||
import * as ReactJSXRuntime from 'react/jsx-runtime';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import * as ReactDomClient from 'react-dom/client';
|
||||
|
||||
const PluginProvider = ({ children }: PropsWithChildren) =>
|
||||
React.createElement(
|
||||
Provider,
|
||||
{
|
||||
store: rootStore,
|
||||
},
|
||||
children
|
||||
);
|
||||
|
||||
console.log('JotaiUtils', JotaiUtils);
|
||||
|
||||
const customRequire = (id: string) => {
|
||||
if (id === '@toeverything/plugin-infra/manager') {
|
||||
return harden(Manager);
|
||||
}
|
||||
if (id === 'react') {
|
||||
return React;
|
||||
}
|
||||
if (id === 'react/jsx-runtime') {
|
||||
return ReactJSXRuntime;
|
||||
}
|
||||
if (id === 'react-dom') {
|
||||
return ReactDom;
|
||||
}
|
||||
if (id === 'react-dom/client') {
|
||||
return ReactDomClient;
|
||||
}
|
||||
if (id === '@blocksuite/icons') {
|
||||
return harden(Icons);
|
||||
}
|
||||
if (id === '@affine/component') {
|
||||
return harden(AFFiNEComponent);
|
||||
}
|
||||
if (id === '@blocksuite/blocks/std') {
|
||||
return harden(BlockSuiteBlocksStd);
|
||||
}
|
||||
if (id === '@blocksuite/global/utils') {
|
||||
return harden(BlockSuiteGlobalUtils);
|
||||
}
|
||||
if (id === 'jotai') {
|
||||
return harden(Jotai);
|
||||
}
|
||||
if (id === 'jotai/utils') {
|
||||
return harden(JotaiUtils);
|
||||
}
|
||||
if (id === '../plugin.js') {
|
||||
return entryCompartment.evaluate('exports');
|
||||
}
|
||||
throw new Error(`Cannot find module '${id}'`);
|
||||
};
|
||||
|
||||
const createGlobalThis = () => {
|
||||
return {
|
||||
process: harden({
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
},
|
||||
}),
|
||||
// UNSAFE: React will read `window` and `document`
|
||||
window,
|
||||
document,
|
||||
navigator,
|
||||
userAgent: navigator.userAgent,
|
||||
|
||||
// fixme: use our own db api
|
||||
indexedDB: globalThis.indexedDB,
|
||||
IDBRequest: globalThis.IDBRequest,
|
||||
IDBDatabase: globalThis.IDBDatabase,
|
||||
IDBCursorWithValue: globalThis.IDBCursorWithValue,
|
||||
IDBFactory: globalThis.IDBFactory,
|
||||
IDBKeyRange: globalThis.IDBKeyRange,
|
||||
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
|
||||
IDBTransaction: globalThis.IDBTransaction,
|
||||
IDBObjectStore: globalThis.IDBObjectStore,
|
||||
IDBIndex: globalThis.IDBIndex,
|
||||
IDBCursor: globalThis.IDBCursor,
|
||||
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
|
||||
|
||||
exports: {},
|
||||
console: globalThis.console,
|
||||
require: customRequire,
|
||||
};
|
||||
};
|
||||
|
||||
const group = new DisposableGroup();
|
||||
const pluginList = await (
|
||||
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
|
||||
).json();
|
||||
const builtInPlugins: string[] = pluginList.map((plugin: any) => plugin.name);
|
||||
const pluginGlobalThis = createGlobalThis();
|
||||
const pluginEntry = await fetch('/plugins/plugin.js').then(res => res.text());
|
||||
const entryCompartment = new Compartment(pluginGlobalThis, {});
|
||||
entryCompartment.evaluate(pluginEntry, {
|
||||
__evadeHtmlCommentTest__: true,
|
||||
});
|
||||
await Promise.all(
|
||||
builtInPlugins.map(plugin => {
|
||||
const pluginCompartment = new Compartment(createGlobalThis(), {});
|
||||
const pluginGlobalThis = pluginCompartment.globalThis;
|
||||
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
|
||||
const packageJsonURL = new URL('package.json', baseURL);
|
||||
return fetch(packageJsonURL).then(async res => {
|
||||
const packageJson = await res.json();
|
||||
const pluginConfig = packageJson['affinePlugin'];
|
||||
if (
|
||||
pluginConfig.release === false &&
|
||||
process.env.NODE_ENV !== 'development'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
|
||||
const coreEntry = new URL(pluginConfig.entry.core, baseURL.toString());
|
||||
const codeText = await fetch(coreEntry).then(res => res.text());
|
||||
pluginCompartment.evaluate(codeText);
|
||||
pluginGlobalThis.__INTERNAL__ENTRY = {
|
||||
register: (part, callback) => {
|
||||
if (part === 'headerItem') {
|
||||
rootStore.set(headerItemsAtom, items => ({
|
||||
...items,
|
||||
[plugin]: callback as CallbackMap['headerItem'],
|
||||
}));
|
||||
} else if (part === 'editor') {
|
||||
rootStore.set(editorItemsAtom, items => ({
|
||||
...items,
|
||||
[plugin]: callback as CallbackMap['editor'],
|
||||
}));
|
||||
} else if (part === 'window') {
|
||||
rootStore.set(windowItemsAtom, items => ({
|
||||
...items,
|
||||
[plugin]: callback as CallbackMap['window'],
|
||||
}));
|
||||
} else {
|
||||
throw new Error(`Unknown part: ${part}`);
|
||||
}
|
||||
},
|
||||
utils: {
|
||||
PluginProvider,
|
||||
},
|
||||
} satisfies PluginContext;
|
||||
const dispose = pluginCompartment.evaluate(
|
||||
'exports.entry(__INTERNAL__ENTRY)'
|
||||
);
|
||||
if (typeof dispose !== 'function') {
|
||||
throw new Error('Plugin entry must return a function');
|
||||
}
|
||||
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
|
||||
group.add(dispose);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
console.log('register plugins finished');
|
||||
|
@ -1,22 +1,25 @@
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { registeredPluginAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
export const Plugins = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const plugins = useAtomValue(affinePluginsAtom);
|
||||
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
||||
console.log('allowedPlugins', allowedPlugins);
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={'Plugins'}
|
||||
subtitle={t['None yet']()}
|
||||
subtitle={allowedPlugins.length === 0 && t['None yet']()}
|
||||
data-testid="plugins-title"
|
||||
/>
|
||||
{Object.values(plugins).map(({ definition, uiAdapter }) => {
|
||||
const Content = uiAdapter.debugContent;
|
||||
return <div key={definition.id}>{Content && <Content />}</div>;
|
||||
})}
|
||||
{allowedPlugins.map(plugin => (
|
||||
<SettingWrapper key={plugin} title={plugin}></SettingWrapper>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -7,20 +7,18 @@ import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
|
||||
import { headerItemsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { contentLayoutAtom } from '../../../atoms/layout';
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import DownloadClientTip from './download-tips';
|
||||
@ -124,41 +122,37 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
|
||||
export type HeaderProps = BaseHeaderProps;
|
||||
|
||||
const PluginHeaderItemAdapter = memo<{
|
||||
headerItem: PluginUIAdapter['headerItem'];
|
||||
}>(function PluginHeaderItemAdapter({ headerItem }) {
|
||||
return (
|
||||
<div>
|
||||
{headerItem({
|
||||
contentLayoutAtom,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const PluginHeader = () => {
|
||||
const affinePluginsMap = useAtomValue(affinePluginsAtom);
|
||||
const plugins = useMemo(
|
||||
() => Object.values(affinePluginsMap),
|
||||
[affinePluginsMap]
|
||||
);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const headerItems = useAtomValue(headerItemsAtom);
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(headerItems).map(([id, headerItem]) => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', id);
|
||||
const cleanup = headerItem(div);
|
||||
root.appendChild(div);
|
||||
return () => {
|
||||
cleanup();
|
||||
root.removeChild(div);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{plugins
|
||||
.filter(plugin => plugin.uiAdapter.headerItem != null)
|
||||
.map(plugin => {
|
||||
const headerItem = plugin.uiAdapter
|
||||
.headerItem as PluginUIAdapter['headerItem'];
|
||||
return (
|
||||
<PluginHeaderItemAdapter
|
||||
key={plugin.definition.id}
|
||||
headerItem={headerItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return () => {
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
disposes.forEach(dispose => dispose());
|
||||
});
|
||||
};
|
||||
}, [headerItems]);
|
||||
|
||||
return <div className={styles.pluginHeaderItems} ref={rootRef} />;
|
||||
};
|
||||
|
||||
export const Header = forwardRef<
|
||||
|
@ -238,3 +238,10 @@ export const windowAppControl = style({
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const pluginHeaderItems = style({
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
|
@ -7,21 +7,22 @@ import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import type {
|
||||
AffinePlugin,
|
||||
LayoutNode,
|
||||
PluginUIAdapter,
|
||||
} from '@toeverything/plugin-infra/type';
|
||||
import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type';
|
||||
import type { CallbackMap } from '@toeverything/plugin-infra/entry';
|
||||
import {
|
||||
affinePluginsAtom,
|
||||
contentLayoutAtom,
|
||||
editorItemsAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import type { AffinePlugin, LayoutNode } from '@toeverything/plugin-infra/type';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { CSSProperties, FC, ReactElement } from 'react';
|
||||
import { memo, Suspense, useCallback, useMemo } from 'react';
|
||||
import { memo, Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import { pageSettingFamily } from '../atoms';
|
||||
import { contentLayoutAtom } from '../atoms/layout';
|
||||
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
import * as styles from './page-detail-editor.css';
|
||||
@ -42,11 +43,6 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
onLoad,
|
||||
isPublic,
|
||||
}: PageDetailEditorProps) {
|
||||
const affinePluginsMap = useAtomValue(affinePluginsAtom);
|
||||
const plugins = useMemo(
|
||||
() => Object.values(affinePluginsMap),
|
||||
[affinePluginsMap]
|
||||
);
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace, pageId);
|
||||
@ -100,33 +96,65 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
if (onLoad) {
|
||||
dispose = onLoad(page, editor);
|
||||
}
|
||||
const uiDecorators = plugins
|
||||
.map(plugin => plugin.blockSuiteAdapter.uiDecorator)
|
||||
.filter((ui): ui is PluginBlockSuiteAdapter['uiDecorator'] =>
|
||||
Boolean(ui)
|
||||
);
|
||||
const disposes = uiDecorators.map(ui => ui(editor));
|
||||
const editorItems = rootStore.get(editorItemsAtom);
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', id);
|
||||
const cleanup = editorItem(div, editor);
|
||||
assertExists(parent);
|
||||
document.body.appendChild(div);
|
||||
return () => {
|
||||
cleanup();
|
||||
document.body.removeChild(div);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposes.forEach(fn => fn());
|
||||
dispose();
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
disposes.forEach(dispose => dispose());
|
||||
});
|
||||
};
|
||||
},
|
||||
[plugins, onLoad]
|
||||
[onLoad]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const PluginContentAdapter = memo<{
|
||||
detailContent: PluginUIAdapter['detailContent'];
|
||||
}>(function PluginContentAdapter({ detailContent }) {
|
||||
return (
|
||||
<div className={pluginContainer}>
|
||||
{detailContent({
|
||||
contentLayoutAtom,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
windowItem: CallbackMap['window'];
|
||||
}>(function PluginContentAdapter({ windowItem }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const root = ref.current;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
let cleanup: () => void = () => {};
|
||||
let childDiv: HTMLDivElement | null = null;
|
||||
const renderTimeout = setTimeout(() => {
|
||||
const div = document.createElement('div');
|
||||
cleanup = windowItem(div);
|
||||
root.appendChild(div);
|
||||
childDiv = div;
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
if (childDiv) {
|
||||
root.removeChild(childDiv);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [windowItem]);
|
||||
return <div className={pluginContainer} ref={ref} />;
|
||||
});
|
||||
|
||||
type LayoutPanelProps = {
|
||||
@ -139,16 +167,13 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
props: LayoutPanelProps
|
||||
): ReactElement {
|
||||
const node = props.node;
|
||||
const windowItems = useAtomValue(windowItemsAtom);
|
||||
if (typeof node === 'string') {
|
||||
if (node === 'editor') {
|
||||
return <EditorWrapper {...props.editorProps} />;
|
||||
} else {
|
||||
const plugin = props.plugins.find(
|
||||
plugin => plugin.definition.id === node
|
||||
);
|
||||
const Content = plugin?.uiAdapter.detailContent;
|
||||
assertExists(Content);
|
||||
return <PluginContentAdapter detailContent={Content} />;
|
||||
const windowItem = windowItems[node];
|
||||
return <PluginContentAdapter windowItem={windowItem} />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
|
@ -8,7 +8,7 @@ import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import { type ReactElement, useCallback, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
|
@ -46,9 +46,6 @@
|
||||
"path": "../../packages/workspace"
|
||||
},
|
||||
|
||||
{
|
||||
"path": "../../plugins/bookmark-block"
|
||||
},
|
||||
{
|
||||
"path": "../../plugins/copilot"
|
||||
},
|
||||
|
@ -19,13 +19,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "dev-core",
|
||||
"dev:plugin": "vite build --watch",
|
||||
"dev:electron": "yarn workspace @affine/electron dev:app",
|
||||
"dev:plugins": "./apps/electron/scripts/plugins/dev-plugins.mjs",
|
||||
"build": "yarn nx build @affine/core",
|
||||
"build:electron": "yarn nx build @affine/electron",
|
||||
"build:storage": "yarn nx run-many -t build -p @affine/storage",
|
||||
"build:infra": "yarn nx run-many -t build -p plugin-infra infra",
|
||||
"build:plugins": "yarn nx run-many -t build -p @affine/bookmark-block",
|
||||
"build:plugins": "yarn workspace @affine/bookmark-block build && yarn vite build",
|
||||
"build:storybook": "yarn nx build @affine/storybook",
|
||||
"start:web-static": "yarn workspace @affine/core static-server",
|
||||
"start:storybook": "yarn exec serve apps/storybook/storybook-static -l 6006",
|
||||
@ -76,7 +77,7 @@
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"@vanilla-extract/vite-plugin": "^3.8.2",
|
||||
"@vanilla-extract/webpack-plugin": "^2.2.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"@vitest/coverage-istanbul": "^0.32.2",
|
||||
"@vitest/ui": "^0.32.2",
|
||||
"eslint": "^8.44.0",
|
||||
@ -106,6 +107,7 @@
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-istanbul": "^4.1.0",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.32.2",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
|
@ -43,6 +43,12 @@ const flags = {
|
||||
coverage: process.env.COVERAGE === 'true',
|
||||
} satisfies BuildFlags;
|
||||
|
||||
spawn('vite', ['build'], {
|
||||
cwd: projectRoot,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
spawn(
|
||||
'node',
|
||||
[
|
||||
|
@ -91,6 +91,12 @@ flags.mode = buildFlags.mode as any;
|
||||
flags.channel = buildFlags.channel as any;
|
||||
flags.coverage = buildFlags.coverage;
|
||||
|
||||
spawn('vite', ['build', '--watch'], {
|
||||
cwd: projectRoot,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
spawn(
|
||||
'node',
|
||||
[
|
||||
|
@ -9,6 +9,11 @@
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
"./entry": {
|
||||
"type": "./dist/entry.d.ts",
|
||||
"import": "./dist/entry.js",
|
||||
"require": "./dist/entry.cjs"
|
||||
},
|
||||
"./manager": {
|
||||
"type": "./dist/manager.d.ts",
|
||||
"import": "./dist/manager.js",
|
||||
|
17
packages/plugin-infra/src/entry.ts
Normal file
17
packages/plugin-infra/src/entry.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { FC } from 'react';
|
||||
|
||||
export type Part = 'headerItem' | 'editor' | 'window';
|
||||
|
||||
export type CallbackMap = {
|
||||
headerItem: (root: HTMLElement) => () => void;
|
||||
window: (root: HTMLElement) => () => void;
|
||||
editor: (root: HTMLElement, editor: EditorContainer) => () => void;
|
||||
};
|
||||
|
||||
export interface PluginContext {
|
||||
register: <T extends Part>(part: T, callback: CallbackMap[T]) => void;
|
||||
utils: {
|
||||
PluginProvider: FC;
|
||||
};
|
||||
}
|
@ -3,17 +3,30 @@ import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { atom, createStore } from 'jotai/vanilla';
|
||||
|
||||
import { getWorkspace, waitForWorkspace } from './__internal__/workspace';
|
||||
import type { AffinePlugin, Definition, ServerAdapter } from './type';
|
||||
import type { CallbackMap } from './entry';
|
||||
import type {
|
||||
AffinePlugin,
|
||||
Definition,
|
||||
ExpectedLayout,
|
||||
ServerAdapter,
|
||||
} from './type';
|
||||
import type { Loader, PluginUIAdapter } from './type';
|
||||
import type { PluginBlockSuiteAdapter } from './type';
|
||||
|
||||
const isServer = typeof window === 'undefined';
|
||||
const isClient = typeof window !== 'undefined';
|
||||
|
||||
// global store
|
||||
export const rootStore = createStore();
|
||||
|
||||
// todo: for now every plugin is enabled by default
|
||||
// id -> HTML element
|
||||
export const headerItemsAtom = atom<Record<string, CallbackMap['headerItem']>>(
|
||||
{}
|
||||
);
|
||||
export const editorItemsAtom = atom<Record<string, CallbackMap['editor']>>({});
|
||||
export const registeredPluginAtom = atom<string[]>([]);
|
||||
export const windowItemsAtom = atom<Record<string, CallbackMap['window']>>({});
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const affinePluginsAtom = atom<Record<string, AffinePlugin<string>>>({});
|
||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||
export const currentPageIdAtom = atom<string | null>(null);
|
||||
@ -39,6 +52,38 @@ export const currentPageAtom = atom<Promise<Page>>(async get => {
|
||||
return page;
|
||||
});
|
||||
|
||||
const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
|
||||
|
||||
type SetStateAction<Value> = Value | ((prev: Value) => Value);
|
||||
export const contentLayoutAtom = atom<
|
||||
ExpectedLayout,
|
||||
[SetStateAction<ExpectedLayout>],
|
||||
void
|
||||
>(
|
||||
get => get(contentLayoutBaseAtom),
|
||||
(get, set, layout) => {
|
||||
set(contentLayoutBaseAtom, prev => {
|
||||
let setV: (prev: ExpectedLayout) => ExpectedLayout;
|
||||
if (typeof layout !== 'function') {
|
||||
setV = () => layout;
|
||||
} else {
|
||||
setV = layout;
|
||||
}
|
||||
const nextValue = setV(prev);
|
||||
if (nextValue === 'editor') {
|
||||
return nextValue;
|
||||
}
|
||||
if (nextValue.first !== 'editor') {
|
||||
throw new Error('The first element of the layout should be editor.');
|
||||
}
|
||||
if (nextValue.splitPercentage && nextValue.splitPercentage < 70) {
|
||||
throw new Error('The split percentage should be greater than 70.');
|
||||
}
|
||||
return nextValue;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export function definePlugin<ID extends string>(
|
||||
definition: Definition<ID>,
|
||||
uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>,
|
||||
@ -47,8 +92,8 @@ export function definePlugin<ID extends string>(
|
||||
) {
|
||||
const basePlugin = {
|
||||
definition,
|
||||
uiAdapter: {},
|
||||
blockSuiteAdapter: {},
|
||||
uiAdapter: undefined,
|
||||
blockSuiteAdapter: undefined,
|
||||
};
|
||||
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
@ -56,76 +101,21 @@ export function definePlugin<ID extends string>(
|
||||
[definition.id]: basePlugin,
|
||||
}));
|
||||
|
||||
if (isServer) {
|
||||
if (serverAdapter) {
|
||||
console.log('register server adapter');
|
||||
serverAdapter
|
||||
.load()
|
||||
.then(({ default: adapter }) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
serverAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
} else if (isClient) {
|
||||
if (blockSuiteAdapter) {
|
||||
const updateAdapter = (adapter: Partial<PluginBlockSuiteAdapter>) => {
|
||||
if (serverAdapter) {
|
||||
console.log('register server adapter');
|
||||
serverAdapter
|
||||
.load()
|
||||
.then(({ default: adapter }) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
blockSuiteAdapter: adapter,
|
||||
serverAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
blockSuiteAdapter
|
||||
.load()
|
||||
.then(({ default: adapter }) => updateAdapter(adapter))
|
||||
.catch(err => {
|
||||
console.error('[definePlugin] blockSuiteAdapter error', err);
|
||||
});
|
||||
|
||||
if (import.meta.webpackHot) {
|
||||
blockSuiteAdapter.hotModuleReload(async _ => {
|
||||
const adapter = (await _).default;
|
||||
updateAdapter(adapter);
|
||||
console.info('[HMR] Plugin', definition.id, 'hot reloaded.');
|
||||
});
|
||||
}
|
||||
}
|
||||
if (uiAdapterLoader) {
|
||||
const updateAdapter = (adapter: Partial<PluginUIAdapter>) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
uiAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
uiAdapterLoader
|
||||
.load()
|
||||
.then(({ default: adapter }) => updateAdapter(adapter))
|
||||
.catch(err => {
|
||||
console.error('[definePlugin] blockSuiteAdapter error', err);
|
||||
});
|
||||
|
||||
if (import.meta.webpackHot) {
|
||||
uiAdapterLoader.hotModuleReload(async _ => {
|
||||
const adapter = (await _).default;
|
||||
updateAdapter(adapter);
|
||||
console.info('[HMR] Plugin', definition.id, 'hot reloaded.');
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ export type ServerAdapter = (affine: AFFiNEServer) => () => void;
|
||||
|
||||
export type AffinePlugin<ID extends string> = {
|
||||
definition: Definition<ID>;
|
||||
uiAdapter: Partial<PluginUIAdapter>;
|
||||
blockSuiteAdapter: Partial<PluginBlockSuiteAdapter>;
|
||||
uiAdapter: undefined;
|
||||
blockSuiteAdapter: undefined;
|
||||
serverAdapter?: ServerAdapter;
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ export default defineConfig({
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: {
|
||||
entry: resolve(root, 'src/entry.ts'),
|
||||
type: resolve(root, 'src/type.ts'),
|
||||
manager: resolve(root, 'src/manager.ts'),
|
||||
'__internal__/workspace': resolve(
|
||||
|
@ -1,5 +1,7 @@
|
||||
# `@affine/bookmark-block`
|
||||
|
||||
> Moved to [@affine/bookmark-plugin](../bookmark)
|
||||
>
|
||||
> A block for bookmarking a website
|
||||
|
||||
![preview](assets/preview.png)
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vitest.config.ts",
|
||||
"scripts",
|
||||
"apps/core/.webpack/runtime-config.ts"
|
||||
|
92
vite.config.ts
Normal file
92
vite.config.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const root = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const builtInPlugins = ['hello-world', 'bookmark', 'copilot'];
|
||||
|
||||
const outputJson: [pluginName: string, output: string][] = [];
|
||||
|
||||
const entry = builtInPlugins.reduce(
|
||||
(acc, plugin) => {
|
||||
const packageJson = require(resolve(
|
||||
root,
|
||||
'plugins',
|
||||
plugin,
|
||||
'package.json'
|
||||
));
|
||||
const entry = packageJson.affinePlugin.entry.core;
|
||||
acc[`${plugin}/index`] = resolve(root, 'plugins', plugin, entry);
|
||||
packageJson.affinePlugin.entry.core = './index.js';
|
||||
outputJson.push([plugin, JSON.stringify(packageJson, null, 2)]);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: resolve(root, 'apps', 'core', 'public', 'plugins'),
|
||||
emptyOutDir: true,
|
||||
minify: false,
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: () => 'plugin',
|
||||
entryFileNames: '[name].js',
|
||||
chunkFileNames: '[name].js',
|
||||
},
|
||||
external: [
|
||||
// built-in packages
|
||||
/^@affine/,
|
||||
/^@blocksuite/,
|
||||
/^@toeverything/,
|
||||
|
||||
// react
|
||||
/^react/,
|
||||
/^react-dom/,
|
||||
|
||||
// store
|
||||
/^jotai/,
|
||||
|
||||
// css
|
||||
/^@vanilla-extract/,
|
||||
],
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
{
|
||||
name: 'generate-manifest',
|
||||
generateBundle() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: `plugin-list.json`,
|
||||
source: JSON.stringify(
|
||||
builtInPlugins.map(plugin => ({
|
||||
name: plugin,
|
||||
}))
|
||||
),
|
||||
});
|
||||
outputJson.forEach(([name, json]) => {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: `${name}/package.json`,
|
||||
source: json,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [react()],
|
||||
});
|
@ -2,7 +2,7 @@ import path, { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const rootDir = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
84
yarn.lock
84
yarn.lock
@ -64,19 +64,23 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/bookmark-block@workspace:*, @affine/bookmark-block@workspace:plugins/bookmark-block":
|
||||
"@affine/bookmark-block@workspace:plugins/bookmark-block":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/bookmark-block@workspace:plugins/bookmark-block"
|
||||
dependencies:
|
||||
"@toeverything/plugin-infra": "workspace:*"
|
||||
link-preview-js: ^3.0.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/bookmark-plugin@workspace:plugins/bookmark":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/bookmark-plugin@workspace:plugins/bookmark"
|
||||
dependencies:
|
||||
"@affine/component": "workspace:*"
|
||||
"@blocksuite/icons": ^2.1.25
|
||||
"@toeverything/plugin-infra": "workspace:*"
|
||||
foxact: ^0.2.11
|
||||
link-preview-js: ^3.0.4
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-dom: "*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -165,8 +169,6 @@ __metadata:
|
||||
"@affine/component": "workspace:*"
|
||||
"@toeverything/plugin-infra": "workspace:*"
|
||||
"@types/marked": ^5.0.0
|
||||
"@types/react": ^18.2.14
|
||||
"@types/react-dom": ^18.2.6
|
||||
idb: ^7.1.1
|
||||
jotai: ^2.2.2
|
||||
langchain: ^0.0.107
|
||||
@ -187,7 +189,6 @@ __metadata:
|
||||
resolution: "@affine/core@workspace:apps/core"
|
||||
dependencies:
|
||||
"@affine-test/fixtures": "workspace:*"
|
||||
"@affine/bookmark-block": "workspace:*"
|
||||
"@affine/component": "workspace:*"
|
||||
"@affine/copilot": "workspace:*"
|
||||
"@affine/debug": "workspace:*"
|
||||
@ -241,6 +242,7 @@ __metadata:
|
||||
react-resizable-panels: ^0.0.53
|
||||
react-router-dom: ^6.14.1
|
||||
rxjs: ^7.8.1
|
||||
ses: ^0.18.5
|
||||
style-loader: ^3.3.3
|
||||
swc-loader: ^0.2.3
|
||||
swc-plugin-coverage-instrument: ^0.0.19
|
||||
@ -373,6 +375,16 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/hello-world-plugin@workspace:plugins/hello-world":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/hello-world-plugin@workspace:plugins/hello-world"
|
||||
dependencies:
|
||||
"@affine/component": "workspace:*"
|
||||
"@blocksuite/icons": ^2.1.25
|
||||
"@toeverything/plugin-infra": "workspace:*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/i18n@workspace:*, @affine/i18n@workspace:packages/i18n":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/i18n@workspace:packages/i18n"
|
||||
@ -434,7 +446,7 @@ __metadata:
|
||||
"@typescript-eslint/parser": ^5.60.1
|
||||
"@vanilla-extract/vite-plugin": ^3.8.2
|
||||
"@vanilla-extract/webpack-plugin": ^2.2.0
|
||||
"@vitejs/plugin-react": ^4.0.1
|
||||
"@vitejs/plugin-react-swc": ^3.3.2
|
||||
"@vitest/coverage-istanbul": ^0.32.2
|
||||
"@vitest/ui": ^0.32.2
|
||||
eslint: ^8.44.0
|
||||
@ -464,6 +476,7 @@ __metadata:
|
||||
typescript: ^5.1.6
|
||||
vite: ^4.3.9
|
||||
vite-plugin-istanbul: ^4.1.0
|
||||
vite-plugin-static-copy: ^0.17.0
|
||||
vite-tsconfig-paths: ^4.2.0
|
||||
vitest: ^0.32.2
|
||||
vitest-fetch-mock: ^0.2.2
|
||||
@ -4657,6 +4670,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@endo/env-options@npm:^0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "@endo/env-options@npm:0.1.1"
|
||||
checksum: c721db3e2d3f9a52a5c391dc8e6d473ffc44cf2bb252fad3048551e35aac52484d612753211116ff9a7157a0768d7c61352c494f491e3463e78fef438beb1d42
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm64@npm:0.17.19":
|
||||
version: 0.17.19
|
||||
resolution: "@esbuild/android-arm64@npm:0.17.19"
|
||||
@ -11385,7 +11405,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core@npm:^1.3.70":
|
||||
"@swc/core@npm:^1.3.61, @swc/core@npm:^1.3.70":
|
||||
version: 1.3.70
|
||||
resolution: "@swc/core@npm:1.3.70"
|
||||
dependencies:
|
||||
@ -12841,6 +12861,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react-swc@npm:^3.3.2":
|
||||
version: 3.3.2
|
||||
resolution: "@vitejs/plugin-react-swc@npm:3.3.2"
|
||||
dependencies:
|
||||
"@swc/core": ^1.3.61
|
||||
peerDependencies:
|
||||
vite: ^4
|
||||
checksum: 1e44b1b7c50b6ae381e2ded9a7971af42286144bf257d7ed1a9dbf8acec0c4af78fe46982941566d31b80b7886649d9b1c06a873571d228e0b8fce68742b9ff4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react@npm:^3.0.1":
|
||||
version: 3.1.0
|
||||
resolution: "@vitejs/plugin-react@npm:3.1.0"
|
||||
@ -19460,8 +19491,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"foxact@npm:^0.2.11":
|
||||
version: 0.2.11
|
||||
resolution: "foxact@npm:0.2.11"
|
||||
version: 0.2.17
|
||||
resolution: "foxact@npm:0.2.17"
|
||||
dependencies:
|
||||
client-only: ^0.0.1
|
||||
server-only: ^0.0.1
|
||||
@ -19470,7 +19501,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
checksum: 711a5d1e287c45eb258a9003910d13fc2c82f3c6cd0e4e9c673dbe64c6a7d95da7cc983b5f143270bc6304b9ceb0de2d25c01588e066e01e8eb0dbb55e55eaa6
|
||||
checksum: 9229ac42032dbce52c95f5583601ba5dbfe8e4bd40b6ec3b372d91793b7b621e01eb45bb953dfe077e65a2ecd4dcdaa42c8f1c60efbf43048a3cd0038a754b60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -29218,6 +29249,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ses@npm:^0.18.5":
|
||||
version: 0.18.5
|
||||
resolution: "ses@npm:0.18.5"
|
||||
dependencies:
|
||||
"@endo/env-options": ^0.1.1
|
||||
checksum: b41faaf28efc97c09b0322de5c12603e015238e4b2cf5195c8fc3f5fb9bc66169dd394c63bcdb30d8fa36a55d03eeda3df64857ab724b2297fb6b60c4f5dc0e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"set-blocking@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "set-blocking@npm:2.0.0"
|
||||
@ -31924,6 +31964,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-plugin-static-copy@npm:^0.17.0":
|
||||
version: 0.17.0
|
||||
resolution: "vite-plugin-static-copy@npm:0.17.0"
|
||||
dependencies:
|
||||
chokidar: ^3.5.3
|
||||
fast-glob: ^3.2.11
|
||||
fs-extra: ^11.1.0
|
||||
picocolors: ^1.0.0
|
||||
peerDependencies:
|
||||
vite: ^3.0.0 || ^4.0.0
|
||||
checksum: b73f001f080517d7c2342ad5b0779c1e5ec8b5820e85a57280e8f979d095946a32ab717a59eaae60e3f715784a138785208619aa6d936cd9a80e6a03e364e4fc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-tsconfig-paths@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "vite-tsconfig-paths@npm:4.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user