feat(editor): add experimental feature adapter panel to AFFiNE canary (#12489)

Closes: [BS-2539](https://linear.app/affine-design/issue/BS-2539/为-affine-添加-ef,并且支持在-affine-预览对应的功能)

> [!warning]
> This feature is only available in the canary build and is intended for debugging purposes.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced an "Adapter Panel" feature with a new sidebar tab for previewing document content in multiple formats (Markdown, PlainText, HTML, Snapshot), controllable via a feature flag.
  - Added a fully integrated adapter panel component with reactive UI elements for selecting adapters, toggling HTML preview modes, and updating content.
  - Provided a customizable adapter panel for both main app and playground environments, supporting content transformation pipelines and export previews.
  - Enabled seamless toggling and live updating of adapter panel content through intuitive menus and controls.

- **Localization**
  - Added English translations and descriptive settings for the Adapter Panel feature.

- **Chores**
  - Added new package and workspace dependencies along with TypeScript project references to support the Adapter Panel modules and components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
donteatfriedrice
2025-05-23 14:08:12 +00:00
parent 2a80fbb993
commit a828c74f87
28 changed files with 970 additions and 308 deletions

View File

@@ -18,6 +18,7 @@ import { TrashPageFooter } from '@affine/core/components/pure/trash-page-footer'
import { TopTip } from '@affine/core/components/top-tip';
import { DocService } from '@affine/core/modules/doc';
import { EditorService } from '@affine/core/modules/editor';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { PeekViewService } from '@affine/core/modules/peek-view';
import { RecentDocsService } from '@affine/core/modules/quicksearch';
@@ -36,6 +37,7 @@ import { DisposableGroup } from '@blocksuite/affine/global/disposable';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
import {
AiIcon,
ExportIcon,
FrameIcon,
PropertyIcon,
TocIcon,
@@ -57,6 +59,7 @@ import { PageNotFound } from '../../404';
import * as styles from './detail-page.css';
import { DetailPageHeader } from './detail-page-header';
import { DetailPageWrapper } from './detail-page-wrapper';
import { EditorAdapterPanel } from './tabs/adapter';
import { EditorChatPanel } from './tabs/chat';
import { EditorFramePanel } from './tabs/frame';
import { EditorJournalPanel } from './tabs/journal';
@@ -103,6 +106,11 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const enableAI = useEnableAI();
const featureFlagService = useService(FeatureFlagService);
const enableAdapterPanel = useLiveData(
featureFlagService.flags.enable_adapter_panel.$
);
useEffect(() => {
if (isActiveView) {
setActiveBlockSuiteEditor(editorContainer);
@@ -360,6 +368,16 @@ const DetailPageImpl = memo(function DetailPageImpl() {
</Scrollable.Root>
</ViewSidebarTab>
{enableAdapterPanel && (
<ViewSidebarTab tabId="adapter" icon={<ExportIcon />}>
<Scrollable.Root className={styles.sidebarScrollArea}>
<Scrollable.Viewport>
<EditorAdapterPanel host={editorContainer?.host ?? null} />
</Scrollable.Viewport>
</Scrollable.Root>
</ViewSidebarTab>
)}
<GlobalPageHistoryModal />
{/* FIXME: wait for better ai, <PageAIOnboarding /> */}
</FrameworkScope>

View File

@@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
height: '100%',
width: '100%',
});

View File

@@ -0,0 +1,74 @@
import { ServerService } from '@affine/core/modules/cloud';
import { AdapterPanel } from '@blocksuite/affine/fragments/adapter-panel';
import {
customImageProxyMiddleware,
docLinkBaseURLMiddlewareBuilder,
embedSyncedDocMiddleware,
titleMiddleware,
} from '@blocksuite/affine/shared/adapters';
import type { EditorHost } from '@blocksuite/affine/std';
import type { TransformerMiddleware } from '@blocksuite/affine/store';
import { useService } from '@toeverything/infra';
import { useCallback, useEffect, useRef } from 'react';
import * as styles from './adapter.css';
const createImageProxyUrl = (baseUrl: string) => {
try {
return new URL(BUILD_CONFIG.imageProxyUrl, baseUrl).toString();
} catch (error) {
console.error('Failed to create image proxy url', error);
return '';
}
};
const createMiddlewares = (
host: EditorHost,
baseUrl: string
): TransformerMiddleware[] => {
const imageProxyUrl = createImageProxyUrl(baseUrl);
return [
docLinkBaseURLMiddlewareBuilder(baseUrl, host.store.workspace.id).get(),
titleMiddleware(host.store.workspace.meta.docMetas),
embedSyncedDocMiddleware('content'),
customImageProxyMiddleware(imageProxyUrl),
];
};
const getTransformerMiddlewares = (
host: EditorHost | null,
baseUrl: string
) => {
if (!host) return [];
return createMiddlewares(host, baseUrl);
};
// A wrapper for AdapterPanel
export const EditorAdapterPanel = ({ host }: { host: EditorHost | null }) => {
const server = useService(ServerService).server;
const adapterPanelRef = useRef<AdapterPanel | null>(null);
const onRefChange = useCallback(
(container: HTMLDivElement | null) => {
if (container && host && container.children.length === 0) {
adapterPanelRef.current = new AdapterPanel();
adapterPanelRef.current.store = host.store;
adapterPanelRef.current.transformerMiddlewares =
getTransformerMiddlewares(host, server.baseUrl);
container.append(adapterPanelRef.current);
}
},
[host, server]
);
useEffect(() => {
if (host && adapterPanelRef.current) {
adapterPanelRef.current.store = host.store;
adapterPanelRef.current.transformerMiddlewares =
getTransformerMiddlewares(host, server.baseUrl);
}
}, [host, server]);
return <div className={styles.root} ref={onRefChange} />;
};

View File

@@ -327,6 +327,15 @@ export const AFFINE_FLAGS = {
configurable: isCanaryBuild,
defaultState: true,
},
enable_adapter_panel: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-adapter-panel.name',
description:
'com.affine.settings.workspace.experimental-features.enable-adapter-panel.description',
configurable: isCanaryBuild,
defaultState: false,
},
} satisfies { [key in string]: FlagInfo };
// oxlint-disable-next-line no-redeclare

View File

@@ -5890,6 +5890,14 @@ export function useAFFiNEI18N(): {
* `Once enabled, you can preview HTML in code block.`
*/
["com.affine.settings.workspace.experimental-features.enable-code-block-html-preview.description"](): string;
/**
* `Adapter Panel`
*/
["com.affine.settings.workspace.experimental-features.enable-adapter-panel.name"](): string;
/**
* `Once enabled, you can preview adapter export content in the right side bar.`
*/
["com.affine.settings.workspace.experimental-features.enable-adapter-panel.description"](): string;
/**
* `Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.`
*/

View File

@@ -1471,6 +1471,8 @@
"com.affine.settings.workspace.experimental-features.enable-table-virtual-scroll.description": "Once enabled, switch table view to virtual scroll mode in Database Block.",
"com.affine.settings.workspace.experimental-features.enable-code-block-html-preview.name": "Code block HTML preview",
"com.affine.settings.workspace.experimental-features.enable-code-block-html-preview.description": "Once enabled, you can preview HTML in code block.",
"com.affine.settings.workspace.experimental-features.enable-adapter-panel.name": "Adapter Panel",
"com.affine.settings.workspace.experimental-features.enable-adapter-panel.description": "Once enabled, you can preview adapter export content in the right side bar.",
"com.affine.settings.workspace.not-owner": "Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.",
"com.affine.settings.workspace.preferences": "Preference",
"com.affine.settings.workspace.billing": "Team's Billing",