dddk — React Adapter

dddk 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/webagent';

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()

拿 dddk instance:

function MyButton() {
  const dddk = useDddk();
  return <button onClick={() => dddk.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]);
}

useA2UI()

渲染 A2UI surface:

function PromptForm() {
  const { mount, unmount } = useA2UI();

  const showForm = () => mount({
    placement: 'center',
    surface: { /* ... */ },
    onSubmit: (data) => console.log(data),
  });

  return <button onClick={showForm}>Open Form</button>;
}

元件

<Palette />

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

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

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

<SubtitleBar />

同上,預設不需要。

<A2UIHost placement="center" />

A2UI surface 的 portal host。預設 dddk 自己掛,但 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 dddk = new DotDotDuck({ /* config */ });
onMounted(() => dddk.mount());
onUnmounted(() => dddk.destroy());
</script>
<!-- Svelte 5 -->
<script>
import { DotDotDuck } from '@perhapxin/dddk';
import { onMount, onDestroy } from 'svelte';

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

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