v
Skip to content

カスタムUI(rootComponent) ​

rootComponent はサンドボックス化されたiframeで動作するReact/TSXファイルの仮想ファイルシステムです。ワールドの視覚レイヤーを提供し、ゲーム状態、チャット制御、セッション管理、AI完了、オーディオへの完全なアクセスを持ちます。

rootComponentスキーマ ​

json
{
  "rootComponent": {
    "id": "uuid",
    "name": "My World UI",
    "entryFile": "index.tsx",
    "files": {
      "index.tsx": "export default function App() { ... }",
      "bubble.tsx": "export default function Bubble({ content, role }) { ... }"
    },
    "updatedAt": "2024-01-01T00:00:00Z"
  }
}

アセットの読み込み ​

rootComponent.assetLoading は任意です。省略時または "automatic" では、従来どおりコード、エントリー、音声、メッセージから先読みのヒントを生成します。現在の段階に必要なアセットだけを読み込む完全なカスタムUIには "on-demand" を指定できます。このワールドの汎用ヒントは、挨拶・メッセージ画像や音声も含めてすべて無効になります。通常のURLと api.fetchAsset() は引き続き使えます。UI自身が読み込み完了、見えるエラー、再試行、段階やセッションの変更時のキャンセル・解放を管理してください。"automatic" に戻すか属性を削除するとヒントが復元されます。再生機能と他のワールドの既定動作は変わりません。

エントリーポイント ​

index.tsx はデフォルトコンポーネントをエクスポートする必要があります。

tsx
export default function App() {
  return <Chat />;
}

カスタムメッセージバブル付き:

tsx
import Bubble from "./bubble";

export default function App() {
  return <Chat renderBubble={Bubble} />;
}

フルアプリモード:

tsx
export default function App() {
  var api = useYumina();
  
  return (
    <div style={ {display: "flex", flexDirection: "column", height: "100vh"} }>
      <header>HP: {api.variables.health}</header>
      <MessageList />
      <MessageInput />
    </div>
  );
}

useYumina() — 完全なAPIリファレンス ​

状態の読み取り ​

typescript
interface SandboxedYuminaAPI {
  // Game state
  variables: Record<string, unknown>;
  globalVariables: Record<string, unknown>;
  
  // World info
  worldName: string;
  worldId: string;
  sessionId: string;
  
  // User identity
  currentUser: { id: string; name?: string; image?: string | null } | null;
  user: { name: string; avatar: string | null };  // Persona-aware
  
  // Chat state
  messages: SandboxMessage[];
  isStreaming: boolean;
  streamingContent: string;
  streamingReasoning: string;
  pendingChoices: string[];
  error: string | null;
  readOnly: boolean;
  
  // Lorebook
  entries: ReadonlyArray<SandboxEntry>;
  getEntry(name: string): SandboxEntry | null;
  
  // Session
  checkpoints: Array<{ id: string; name: string; messageCount: number; createdAt: string }>;
  greetingContent: string | null;
  mode: "session" | "guest-preview";
  capabilities: {
    canSendMessage: boolean;
    canPersistSession: boolean;
    canUseSessionApis: boolean;
    requiresAuth: boolean;
  };
  
  // UI state
  canvasMode: "chat" | "custom" | "fullscreen";
  selectedModel: string;
  userPlan: string;
  preferredProvider: "official" | "private";
  language: string;
  // Audio volumes are not exposed as direct properties — read them via
  //   getAudioVolume("bgm"): number
  //   getAudioVolume("sfx"): number
}

チャットアクション ​

typescript
type ChatImageInput = { type: "image"; mimeType: string; name: string; data: string };
sendMessage(text: string, attachments?: ChatImageInput[]): void;
editMessage(messageId: string, content: string): Promise<boolean>;
deleteMessage(messageId: string): Promise<boolean>;
regenerateMessage(messageId: string): void;
continueLastMessage(): void;
stopGeneration(): void;
restartChat(): void;
swipeMessage(messageId: string, direction: "left" | "right"): Promise<Record<string, unknown>>;
setComposerDraft(text: string): void;
clearPendingChoices(): void;

セッション管理 ​

typescript
revertToMessage(messageId: string): Promise<void>;
branchFromMessage(messageId: string): Promise<string | null>;
getBranchContext(): Promise<BranchContext>;
createSession(worldId: string): Promise<string>;
deleteSession(sessionId: string): Promise<void>;
listSessions(worldId: string): Promise<Array<Record<string, unknown>>>;
navigate(path: string): void;

チェックポイント ​

typescript
saveCheckpoint(): Promise<void>;
loadCheckpoints(): Promise<void>;
restoreCheckpoint(checkpointId: string): Promise<void>;
deleteCheckpoint(checkpointId: string): Promise<void>;

AI完了 ​

typescript
ai.complete(params: {
  messages: Array<{
    role: string;
    content: string | Array<{ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }>;
    attachments?: ChatImageInput[];
  }>;
  onDelta?: (text: string) => void;
  model?: string;
  maxTokens?: number;
  temperature?: number;
  includeLorebook?: boolean | "all" | "matched";
}): Promise<string>;

任意のストリーミングとロアブック注入で生のLLM呼び出しを行います。NPCジェネレーター、動的説明、ヒントシステム、またはメインチャットフロー外の任意のAIロジックに使います。

両APIのユーザーメッセージは attachments で画像を渡せます。data には data:...;base64, の接頭辞を除いたbase64を渡してください。sendMessage("", attachments) なら画像だけを送信できます。1リクエストにつきPNG/JPEG/WebP/GIFを最大4枚、1枚8 MB、合計16 MBまで受け付けます。ai.complete は順序付きの text/image_url も受け付け、画像URLにはdata URLまたはYuminaの公開 /cdn/key/ URLを使えます。<img> の表示や通常のテキストにURLを書くことだけでは、画像はモデルに届きません。画像入力に対応したモデルを使い、失敗時は下書きを保持してください。supportsImages は対応状況が判明している場合だけ提供され、省略されていても画像対応を意味しません。

ゲームアクション ​

typescript
setVariable(id: string, value: unknown, options?: {
  scope?: string;
  targetUserId?: string;
}): void;
executeAction(actionId: string): void;
injectContext(message: string, options?: { role?: "system" | "user" }): void;

オーディオ ​

typescript
playAudio(trackId: string, opts?: {
  volume?: number;
  fadeDuration?: number;   // 秒
  chainTo?: string;
  maxDuration?: number;    // 秒
  duckBgm?: boolean;
  loop?: boolean;          // このトラックの loop 設定を今回の再生だけ上書きする
}): void;
stopAudio(trackId?: string, fadeDuration?: number): void;  // fadeDuration は秒;要素を破棄する
pauseAudio(trackId: string): void;   // その場で一時停止し、位置を保持
resumeAudio(trackId: string): void;  // pauseAudio で一時停止したトラックを再開
onAudioEnded(cb: (trackId: string) => void): () => void;  // ループしないトラックの再生終了時に発火、解除用の関数を返す
setAudioVolume(type: "bgm" | "sfx", volume: number): void;
getAudioVolume(type: "bgm" | "sfx"): number;

ストレージ(ワールドスコープ、永続) ​

typescript
storage.get(key: string): Promise<string | null>;
storage.set(key: string, value: string): Promise<void>;
storage.remove(key: string): Promise<void>;

UIコントロール ​

typescript
toggleImmersive(): void;
openPersonaManager(): void;
getPersonaProfile(): Promise<{ name: string; appearance: string; personality: string; backstory: string; entries?: Array<{ title: string; content: string }> } | null>;
openSupport(): Promise<{ opened: boolean; reason?: "self" | "signed-out" | "unavailable" }>;
fetchAsset(ref: string): Promise<{ ok: boolean; bytes?: ArrayBuffer; contentType?: string; error?: string }>;
switchGreeting(index: number): void;
copyToClipboard(text: string): void;
showToast(message: string, type?: "success" | "error" | "info"): void;
resolveAssetUrl(ref: string): string;
renderMarkdown(text: string): string;

getPersonaProfile() は現在のセッションで選択したペルソナのコピーを明示的に取り込みます。カスタム項目を含み、個人用メモは含みません。「ペルソナなし」では null を返し、失敗時はPromiseを拒否します。プレイヤーが取り込みを選んだときに呼び出し、内容を確認してプレイごとにコピーを保存してください。取り込み済みのコピーは再度取り込むまで更新されません。古いホストには entries やこのメソッドがない場合があります。ペルソナの項目はワールドのロアブック api.entries とは別です。

モデル選択 ​

typescript
setModel(modelId: string): void;
getModels(): Promise<{
  models: Array<{ id: string; name: string; provider: string; contextLength: number; supportsImages?: boolean }>;
  pinnedModels: string[];
  recentlyUsed: string[];
}>;
pinModel(modelId: string): void;
unpinModel(modelId: string): void;
setPreferredProvider(provider: "official" | "private"): Promise<{
  ok: boolean;
  provider?: string;
  error?: string;
}>;

組み込みコンポーネント ​

グローバルとして利用可能。インポート不要です。importステートメントはコンパイル時に静かに取り除かれるので、両方のスタイルが動作しますが、コンポーネントは自動的にスコープに注入されます。

tsx
// これらはすでにスコープ内にあります。そのまま使ってください:
// Chat, MessageList, MessageInput, ChatCanvas,
// ModelPickerModal, ModelTrigger, useAssetFont, Icons

// importステートメントは無害です(コンパイル時に取り除かれる)が、不要です:
// import { Chat } from "yumina/Chat";  ← 動作するが不要

Chatのprops ​

typescript
interface ChatProps {
  renderBubble?: (props: BubbleProps) => React.ReactNode;
  className?: string;
  children?: React.ReactNode;
}

BubbleProps ​

typescript
interface BubbleProps {
  contentHtml: string;
  content: string;
  rawContent: string;
  role: "user" | "assistant" | "system";
  messageIndex: number;
  isStreaming: boolean;
  stateSnapshot: Record<string, unknown> | null;
  variables: Record<string, unknown>;
  renderMarkdown: (text: string) => string;
}

SandboxMessage ​

typescript
interface SandboxMessage {
  id: string;
  sessionId: string;
  role: "user" | "assistant" | "system";
  content: string;
  status?: "complete" | "streaming" | "failed";
  errorMessage?: string | null;
  stateChanges?: Record<string, unknown> | null;
  stateSnapshot?: Record<string, unknown> | null;
  swipes?: Array<{ content: string; stateSnapshot?: Record<string, unknown> | null }>;
  activeSwipeIndex?: number;
  model?: string | null;
  tokenCount?: number | null;
  generationTimeMs?: number | null;
  compacted?: boolean;
  attachments?: Array<{ type: string; mimeType: string; name: string; url: string }> | null;
  createdAt: string;
}

SandboxEntry ​

typescript
interface SandboxEntry {
  id: string;
  name: string;
  content: string;
  keywords: string[];
  position: number;
  section: "system-presets" | "examples" | "chat-history" | "post-history";
  enabled: boolean;
  role: string;
  tags?: string[];
}

グローバルAPI(非React) ​

js
window.yumina              // useYumina()と同じAPI
window.yumina.onChange(cb)  // 状態変更を購読、購読解除関数を返す
window.yumina.offChange(cb) // 購読解除
// windowで "yumina:statechange" イベントもディスパッチ

サンドボックス環境 ​

Reactはグローバルで利用可能です(インポート不要)。React.useState、React.useEffect などを使ってください。

制限 ​

  • fetch / XMLHttpRequest なし
  • 直接の localStorage / sessionStorage なし(代わりに storage.* APIを使う)
  • window.location 操作なし(navigate() を使う)
  • window.parent アクセスなし
  • eval / new Function なし
  • クッキーアクセスなし

互換シム ​

SDKなしの古いワールドコードでも以下を使えます。

  • fetch('/api/*') → 認証情報付きで親経由でプロキシ
  • localStorage / sessionStorage → ワールドスコープ、親にプロキシ
  • navigator.clipboard.writeText() → プロキシ
  • window.location → 合成オブジェクト

スタイリング ​

サンドボックスではTailwind CSSが完全に利用可能です。任意のユーティリティクラスを使ってください(flex、gap-4、text-white、bg-[#1a1a2e] など)。インラインスタイルも機能します。

tsx
// Tailwindクラス(推奨)
<div className="flex flex-col gap-4 p-4 bg-[#1a1a2e] text-[#e0e0e0] font-serif">

// インラインスタイル(これも問題ない)
var style = {
  background: "#1a1a2e",
  color: "#e0e0e0",
  fontFamily: '"Noto Serif SC", serif',
  padding: "16px",
};

複数ファイル構造 ​

tsx
// index.tsx
import StatusPanel from "./status-panel";
import MapView from "./map-view";

export default function App() {
  return (
    <>
      <StatusPanel />
      <Chat />
      <MapView />
    </>
  );
}