カスタム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"
}
}エントリーポイント
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
sendMessage(text: string): 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 }>;
onDelta?: (text: string) => void;
model?: string;
maxTokens?: number;
temperature?: number;
includeLorebook?: boolean | "all" | "matched";
}): Promise<string>;任意のストリーミングとロアブック注入で生のLLM呼び出しを行います。NPCジェネレーター、動的説明、ヒントシステム、またはメインチャットフロー外の任意のAIロジックに使います。
ゲームアクション
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;
switchGreeting(index: number): void;
copyToClipboard(text: string): void;
showToast(message: string, type?: "success" | "error" | "info"): void;
resolveAssetUrl(ref: string): string;
renderMarkdown(text: string): string;モデル選択
typescript
setModel(modelId: string): void;
getModels(): Promise<{
models: Array<{ id: string; name: string; provider: string; contextLength: number }>;
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 />
</>
);
}