dotdotduck — React Adapter

dotdotduck core 是純 DOM + event emitter,無框架。React adapter 把它包成 React hooks 讓 React 用戶更舒服。Vue / Svelte 用戶可以直接用 core API,未來有需求再加 adapter。

安裝

npm install @perhapxin/dddk @perhapxin/dddk-react react

Provider 設定

import { DddkProvider } from '@perhapxin/dddk-react';
import { OpenAIProvider } from '@perhapxin/dddk';

const llm = new OpenAIProvider({ apiKey: 'sk-...' });

function App() {
  return (
    <DddkProvider
      llm={llm}
      locale="zh-TW"
      skills={[introduce, translate]}
    >
      <Router>
        {/* your app */}
      </Router>
    </DddkProvider>
  );
}

DddkProvider 內部建一個 DotDotDuck instance,mount 到 document,並用 React Context 提供給 children。

Hooks

useDddk()

拿 dotdotduck instance:

function MyButton() {
  const dotdotduck = useDddk();
  return <button onClick={() => dotdotduck.palette.toggle()}>Open Palette</button>;
}

useSubtitle()

訂閱字幕條狀態 + 顯示 / 隱藏:

function MyComponent() {
  const { text, type, visible, show, hide } = useSubtitle();

  return (
    <button onClick={() => show({ text: 'Hello!', type: 'info' })}>
      Show Subtitle
    </button>
  );
}

useAgent()

訂閱 webagent 狀態 + 控制:

function AgentControl() {
  const { status, currentStep, subtitle, run, stop } = useAgent();

  return (
    <div>
      <p>Status: {status}</p>
      <p>Subtitle: {subtitle}</p>
      <button onClick={() => run('翻譯這頁')}>Run</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useSkill(id)

跑特定 skill:

function OnboardingButton() {
  const runSkill = useSkill('introduce');
  return <button onClick={runSkill}>Re-run Onboarding</button>;
}

usePalette()

控制 palette:

function MyShortcut() {
  const { open, close, isOpen } = usePalette();

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === '?' && e.ctrlKey) open();
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [open]);
}

useSurface()

Listen for and render Surfaces emitted by skills / webagent:

function SurfaceLayer() {
  const surface = useSurface(); // null when nothing pending

  if (!surface) return null;
  return (
    <Modal onClose={() => surface.dismiss()}>
      <PieceRenderer
        surface={surface.payload}
        catalog={catalog}
        onAction={(action, data) => surface.respond(action, data)}
      />
    </Modal>
  );
}

Under the hood useSurface subscribes to the surface event and exposes the most recent payload + helpers. See surface-renderer for the data shape.

元件

<Palette />

如果要在 React tree 內 render palette(而非 dotdotduck 自掛 overlay),可以用元件版:

<DddkProvider llm={llm}>
  <Palette renderItem={(item) => <MyItem {...item} />} />
  <App />
</DddkProvider>

預設不需要這個 — palette 自己 render 到 document.body

<SubtitleBar />

同上,預設不需要。

<SurfaceHost placement="center" />

Surface 的 portal host — pre-wires useSurface() and PieceRenderer into a chosen placement. React 用戶可控制 portal target。

SSR

DddkProvider 內部用 useEffect mount,SSR 時 skip → 不會 hydration 衝突。Next.js / Remix / SvelteKit 都 OK。

跟 Next.js App Router 整合

// app/providers.tsx
'use client';

import { DddkProvider } from '@perhapxin/dddk-react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <DddkProvider /* ... */>
      {children}
    </DddkProvider>
  );
}

// app/layout.tsx
import { Providers } from './providers';

export default function Layout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Next.js router 整合:

'use client';

import { useRouter } from 'next/navigation';
import { DddkProvider } from '@perhapxin/dddk-react';

export function Providers({ children }) {
  const router = useRouter();
  return (
    <DddkProvider onNavigate={(path) => router.push(path)}>
      {children}
    </DddkProvider>
  );
}

為什麼分開包 React adapter

  • 純 core 用戶不必拉 React peer dep
  • React 用戶可以雙包都裝
  • Vue / Svelte adapter 未來照樣加

@perhapxin/dddk-react 的 peer deps:

{
  "peerDependencies": {
    "@perhapxin/dddk": "^0.1.0",
    "react": "^18.0.0 || ^19.0.0"
  }
}

Vue / Svelte 怎麼辦

直接用 core API:

<!-- Vue 3 -->
<script setup>
import { DotDotDuck } from '@perhapxin/dddk';
import { onMounted, onUnmounted } from 'vue';

const dotdotduck = new DotDotDuck({ /* config */ });
onMounted(() => dotdotduck.mount());
onUnmounted(() => dotdotduck.destroy());
</script>
<!-- Svelte 5 -->
<script>
import { DotDotDuck } from '@perhapxin/dddk';
import { onMount, onDestroy } from 'svelte';

const dotdotduck = new DotDotDuck({ /* config */ });
onMount(() => dotdotduck.mount());
onDestroy(() => dotdotduck.destroy());
</script>

要 hook-like DX 自己包 composable / store 即可。