# World Schema A Yumina world is stored as a JSON object called `WorldDefinition`. This is the complete schema. ## Top-Level Structure ```json { "id": "uuid", "version": "20.0.0", "name": "World Name", "description": "Short description", "author": "Creator Name", "language": "en", "entries": [], "variables": [], "rules": [], "reactions": [], "rootComponent": null, "components": [], "audioTracks": [], "bgmPlaylist": null, "conditionalBGM": [], "customUI": [], "entryFolders": [], "customTags": [], "customTagColors": {}, "editorMode": "advanced", "settings": { "maxTokens": 12000, "maxContext": 200000, "temperature": 1.0, "topP": 1, "frequencyPenalty": 0, "presencePenalty": 0, "playerName": "User", "lorebookScanDepth": 2, "lorebookRecursionDepth": 0 } } ``` ## Field Reference ### Identity | Field | Type | Description | |-------|------|-------------| | `id` | string | UUID, auto-generated | | `version` | string | Schema version, currently `"20.0.0"` | | `name` | string | World name (1-200 chars) | | `description` | string | Short description (0-10,000 chars) | | `author` | string | Creator's display name | | `language` | string (optional) | BCP 47 language code (`"en"`, `"zh"`, `"ja"`, etc.). Present in TypeScript interface but optional in Zod schema. | ### Content | Field | Type | Description | |-------|------|-------------| | `entries` | WorldEntry[] | All content the AI reads — characters, lore, rules, greetings | | `variables` | Variable[] | Game state definitions with behavior rules | | `rules` | Rule[] | WHEN/IF/THEN automation triggers | | `reactions` | Reaction[] (optional) | Event-pattern-based rules (newer system). Can be undefined. | ### Presentation | Field | Type | Description | |-------|------|-------------| | `rootComponent` | RootComponent \| null | Custom UI virtual filesystem (React/TSX) | | `components` | GameComponent[] | Built-in UI components (stat bars, etc.) | | `audioTracks` | AudioTrack[] | BGM, SFX, and ambient tracks | | `bgmPlaylist` | BGMPlaylist \| null | Auto-play music configuration | | `conditionalBGM` | ConditionalBGM[] | State-triggered music | | `customUI` | CustomUIComponent[] | Legacy UI system (use rootComponent instead) | ### Organization | Field | Type | Description | |-------|------|-------------| | `entryFolders` | EntryFolder[] | Folder structure for organizing entries | | `customTags` | string[] | Creator-defined tags for entries | | `customTagColors` | Record\ | Tailwind color classes for custom tags | | `editorMode` | `"simple"` \| `"advanced"` | Editor complexity level | ### Settings | Field | Type | Default | Description | |-------|------|---------|-------------| | `maxTokens` | number | 12000 | Max tokens per AI response | | `maxContext` | number | 200000 | Max context window size | | `temperature` | number | 1.0 | AI creativity (0.0-2.0) | | `topP` | number | 1 | Nucleus sampling threshold | | `frequencyPenalty` | number | 0 | Penalty for repeated tokens | | `presencePenalty` | number | 0 | Penalty for already-used tokens | | `playerName` | string | "User" | Default {{user}} replacement | | `lorebookScanDepth` | number | 2 | How many recent turns to scan for keywords | | `lorebookRecursionDepth` | number | 0 | How many levels of triggered-entries-triggering-entries | | `topK` | integer (optional) | — | Limits candidate token count (min: 0) | | `minP` | float (optional) | — | Minimum probability threshold (0-1) | | `structuredOutput` | boolean (optional) | false | Forces JSON output format | --- # Entries & Sections Entries are discrete content blocks the AI reads when generating each response. Every entry specifies **what** it contains, **when** the AI sees it, and **where** it's placed in the prompt. ## Entry Schema ```json { "id": "unique-kebab-case-id", "name": "Display Name", "content": "The text content the AI reads...", "role": "character", "apiRole": "system", "section": "system-presets", "position": 0, "enabled": true, "keywords": [], "conditions": [], "conditionLogic": "all", "depth": null, "matchWholeWords": false, "secondaryKeywords": [], "secondaryKeywordLogic": "AND_ANY", "preventRecursion": false, "excludeRecursion": false, "tags": [], "folderId": null } ``` ## Sections Entries are categorized into four delivery zones: ### `system-presets` — Always Sent Included in every AI call. Use for essential world content. - `alwaysSend`: true (automatic) - Sorted by `position` (ascending) - Placed at the top of the AI's context ### `examples` — Dialogue Samples Parsed into user/assistant message pairs with an `[Example Chat]` marker. Shows the AI how characters should sound. - `alwaysSend`: true (automatic) - Content format: `` separator between examples, lines alternating with character names ### `chat-history` — Keyword-Triggered, Depth-Injected Only included when keywords match recent messages. Injected at a specific depth within the conversation history. - `alwaysSend`: false (automatic) - `depth`: number of messages from the end where this entry is inserted (0 = right before the latest message) - `keywords`: array of trigger words (any match activates) ### `post-history` — Final Emphasis Placed after all chat messages — the very last content before the AI generates. Gets the most attention. - `alwaysSend`: true (automatic) - Common use: output format instructions, style enforcement, CoT bypass ## Entry Roles | Role | Purpose | |------|---------| | `system` | Narrator instructions, game rules, world mechanics | | `character` | Character descriptions and personalities | | `personality` | Character personality traits (separate from description) | | `scenario` | Setting, situation, scene context | | `lore` | World history, factions, background knowledge | | `plot` | Story progression, quest hooks, events | | `style` | Writing style, tone, formatting | | `example` | Sample dialogue (use with `examples` section) | | `greeting` | First messages on session start (special handling) | | `custom` | Anything that doesn't fit above | ## API Roles The `apiRole` field controls what LLM message role the entry is sent as: | apiRole | Sent As | When to Use | |---------|---------|-------------| | `system` (default) | System message | Most entries | | `user` | User message | CoT bypass prompts, fake user context | | `assistant` | Assistant message | Example responses, tone setting | ## Keyword Matching For `chat-history` entries: ### Primary Keywords Array of strings. **Any** match triggers the entry. ```json "keywords": ["tavern", "inn", "bar", "drink"] ``` ### Secondary Keywords Additional filter with logic operators: | Logic | Meaning | |-------|---------| | `AND_ANY` | Primary matches AND at least one secondary matches | | `AND_ALL` | Primary matches AND all secondaries match | | `NOT_ANY` | Primary matches AND none of the secondaries match | | `NOT_ALL` | Primary matches AND not all secondaries match | ### Recursion Control - `preventRecursion: true` — This entry's content won't trigger other entries - `excludeRecursion: true` — This entry can't be triggered by other entries' content (only by player/AI messages) ## Conditions Entries can have state-based conditions that must be true for the entry to be included: ```json "conditions": [ { "variableId": "story-phase", "operator": "eq", "value": "act2" } ], "conditionLogic": "all" ``` Operators: `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `contains` ## Official Presets Yumina includes 5 optional built-in presets that creators can enable: | Preset | Section | Position | Purpose | |--------|---------|----------|---------| | Fiction Mode | system-presets | 0 | Engage with all content naturally | | Task | system-presets | 1 | "You are the narrator" instruction | | Instructions | system-presets | 2 | Show don't tell, end mid-scene | | Style | system-presets | 3 | Characters have independent voices | | CoT Bypass | post-history | 0 | Post-history jailbreak (user role) | ## Prompt Assembly Order The engine collects entries and assembles them in this order: 1. **System Presets** entries (sorted by position, ascending) 2. **Active persona** block (player's name, appearance, backstory) 3. **Static format block** (behavior rules, directive syntax, audio tracks) 4. **Example** entries (parsed into dialogue pairs) 5. *— cache breakpoint —* 6. **Triggered** system-block entries (keyword-matched) 7. `[Start a new Chat]` marker 8. **Story compaction** summary (compressed old messages) 9. **Session memory** block 10. **Chat history** with depth-injected entries 11. **Pending context** effects (from previous turn's rules) 12. **Dynamic format block** (current variable values) 13. **Post-history** entries (sorted by position) Items closer to the bottom get more AI attention. --- # Variables & Directives Variables are the world's game state. The AI reads current values each turn and writes bracket directives in its response to update them. ## Variable Schema ```json { "id": "health", "name": "Health", "type": "number", "defaultValue": 100, "min": 0, "max": 100, "description": "Player's physical health", "category": "stat", "behaviorRules": "0 = death. 1-20 = critical. 20-50 = wounded. 50-80 = bruised. 80-100 = healthy. Decrease on physical damage: punch -5 to -10, slash -15 to -25, fall -20 to -40. Rest +5, healing +10 to +30. Max change per turn: 30." } ``` ## Variable Types ### `number` Numeric values with optional min/max bounds. **Directive syntax:** ``` [health: set 50] → set to 50 [health: +10] → add 10 (alias: add) [health: -15] → subtract 15 (alias: subtract) [health: *2] → multiply by 2 (alias: multiply) [gold: 100] → implicit set (no operator) ``` ### `string` Text values. **Directive syntax:** ``` [location: set "dark forest"] [mood: set "suspicious"] [notes: append " Found a clue."] ``` ### `boolean` True/false flags. **Directive syntax:** ``` [has-key: toggle] → flip true ↔ false [met-elder: set true] [quest-active: set false] ``` ### `json` Complex structures — objects and arrays. Supports nested dot-path updates. **Directive syntax:** ``` [inventory: push {"name": "Iron Sword", "damage": 10}] [inventory: delete 0] [inventory: set [{"name": "Potion", "qty": 3}]] [npcs.aria.affinity: +5] [npcs.aria.mood: set "happy"] [npcs: merge {"aria": {"trust": 80}}] [quest-log: push {"id": "q1", "status": "active"}] [config: delete "deprecated-key"] ``` **Dot-path operations:** - `[root.nested.field: op value]` — updates a nested field within a JSON variable - Auto-creates intermediate objects if they don't exist - Array index access: `[inventory.0.durability: -1]` ## All Operations | Operation | Types | Syntax | Behavior | |-----------|-------|--------|----------| | `set` | all | `[id: set value]` or `[id: value]` | Replace value | | `add` / `+` | number | `[id: +10]` or `[id: add 10]` | Increment | | `subtract` / `-` | number | `[id: -5]` or `[id: subtract 5]` | Decrement | | `multiply` / `*` | number | `[id: *2]` or `[id: multiply 2]` | Scale | | `toggle` | boolean | `[id: toggle]` | Flip true/false | | `append` | string | `[id: append " text"]` | Concatenate | | `merge` | json (object) | `[id: merge {"key": "val"}]` | Shallow merge | | `push` | json (array) | `[id: push "item"]` or `[id: push {...}]` | Append to array | | `delete` | json | `[id: delete "key"]` or `[id: delete 0]` | Remove key/index | ## Audio Directives Audio is triggered through a separate directive format: ``` [audio: track-id play] [audio: track-id stop] [audio: track-id crossfade 2.5] [audio: track-id volume 0.5] [audio: track-id play chain:next-track] ``` ## Behavior Rules The `behaviorRules` field is plain English that gets injected into the AI's system prompt as a `` block. It teaches the AI when and how to update the variable. **Effective behavior rules include:** - What each value range means narratively - What triggers changes (and in which direction) - Magnitude guidelines (how much to change) - Limits (max change per turn, absolute bounds) - Relationships with other variables **Example — Complex JSON variable:** ``` "behaviorRules": "Array of party members. Push new object when recruiting: {\"name\": \"...\", \"class\": \"...\", \"trust\": 50}. Update trust via dot-path: [allies.0.trust: +5]. Remove via index: [allies: delete 0]. Trust < 20 = may betray." ``` ## Variable Categories > **Note:** `category` is accepted by Studio AI tools for organizational purposes but is **NOT** part of the world schema and will be stripped during validation. It does not appear in the Zod schema. | Category | Typical Use | |----------|-------------| | `stat` | Health, mana, stamina, level | | `inventory` | Items, equipment, resources | | `resource` | Gold, energy, food, materials | | `flag` | Quest completion, met NPCs, discovered locations | | `relationship` | NPC affinity, faction standing, trust | | `custom` | Anything else | ## ID Conventions - Use **kebab-case**: `player-health`, `day-count`, `npc-trust` - IDs must be unique within the world - IDs become macro names: variable `player-health` is accessible as {{player-health}} in entries - Avoid reserved words: `audio`, `user`, `char`, `time`, `date` --- # Rules & Reactions Rules fire automatically between turns based on events. They handle precise mechanics without AI involvement. ## Rule Schema ```json { "id": "low-health-warning", "name": "Low Health Warning", "description": "Warn the player when health is critical", "trigger": { "type": "variable-crossed", "variableId": "health", "direction": "drops-below", "threshold": 20 }, "conditions": [], "conditionLogic": "all", "actions": [ { "type": "notify-player", "style": "warning", "message": "Your vision blurs. You're barely standing." }, { "type": "inject-directive", "directiveId": "critical-health", "content": "The player is near death. Describe their physical deterioration — stumbling, blurred vision, trembling hands.", "position": "after_char", "persistent": true } ], "priority": 50, "enabled": true, "cooldownTurns": null, "maxFireCount": null } ``` ## Trigger Types | Type | Fields | Fires When | |------|--------|-----------| | `variable-crossed` | `variableId`, `direction` (`rises-above` \| `drops-below`), `threshold` | Variable crosses a threshold | | `state-change` | `variableId` (optional) | Any variable changes (or a specific one) | | `turn-count` | `atTurn` (number), `everyNTurns` (number) | At a specific turn number, or every N turns | | `session-start` | — | First message of a new session | | `keyword` | `keywords[]` | Player message contains any keyword | | `ai-keyword` | `keywords[]` | AI response contains any keyword | | `every-turn` | — | After every single turn | | `action` | `actionId` (string) | A specific action is executed (e.g., from a custom UI button) | | `manual` | — | Only fires when triggered by another rule | ## Conditions Optional state checks that must pass for the rule to fire: ```json "conditions": [ { "variableId": "story-phase", "operator": "eq", "value": "act2" }, { "variableId": "health", "operator": "gt", "value": 0 } ] ``` Operators: `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `contains` `conditionLogic`: `"all"` (AND) or `"any"` (OR) ## Action Types ### `modify-variable` Change a variable value. ```json { "type": "modify-variable", "variableId": "hunger", "operation": "subtract", "value": 5 } ``` Operations: `set`, `add`, `subtract`, `multiply`, `toggle`, `append`, `merge`, `push`, `delete` ### `inject-directive` Add instructions to the AI's prompt for future turns. ```json { "type": "inject-directive", "directiveId": "romance-mode", "content": "The character has developed feelings. Write romantic tension naturally.", "position": "after_char", "persistent": true } ``` Positions: `top`, `before_char`, `after_char`, `auto`, `depth`, `bottom` `persistent: true` keeps the directive active until explicitly removed. `persistent: false` (default) = one-shot, removed after one turn. `duration`: optional number. When set, the directive auto-removes after N turns. ### `remove-directive` Remove a previously injected directive. ```json { "type": "remove-directive", "directiveId": "romance-mode" } ``` ### `notify-player` Show a toast notification. ```json { "type": "notify-player", "style": "warning", "message": "You're running low on supplies." } ``` Styles: `info`, `achievement`, `warning`, `danger` ### `play-audio` Trigger an audio track. ```json { "type": "play-audio", "trackId": "bgm-battle", "action": "play" } ``` ### `toggle-entry` Enable or disable a lorebook entry. ```json { "type": "toggle-entry", "entryId": "secret-lore", "enabled": true } ``` ### `toggle-rule` Enable or disable another rule (chain reactions). ```json { "type": "toggle-rule", "ruleId": "phase-2-triggers", "enabled": true } ``` ### `send-context` Send a one-shot context message to the AI on the next turn. ```json { "type": "send-context", "message": "The player just triggered a hidden event. React accordingly.", "role": "system" } ``` `role`: `"system"` (default) or `"user"` — determines how the context is injected into the conversation. ## Rule Options | Field | Type | Description | |-------|------|-------------| | `priority` | number | Higher = fires first when multiple rules trigger (default: 0) | | `cooldownTurns` | number \| null | Minimum turns between fires | | `maxFireCount` | number \| null | Total times this rule can ever fire (null = unlimited) | | `enabled` | boolean | Can be toggled by other rules | ## Reactions (Event Pattern System) Reactions are a newer, more flexible rule system based on generic event patterns: ```json { "id": "zone-enter-forest", "name": "Enter Forest", "when": { "eventType": "spatial:zone-enter", "zone": "forest" }, "conditions": [], "conditionLogic": "all", "then": [ { "type": "set", "path": "weather", "value": "foggy" }, { "type": "set", "path": "@audio.ambient", "value": "forest-ambient", "operation": "set" }, { "type": "emit", "event": { "type": "spatial:ambience-changed" } } ] } ``` ### System Effect Paths Reactions can target system features via `@` paths: | Path | Effect | |------|--------| | `@audio.bgm` | Play BGM track | | `@audio.sfx` | Play SFX | | `@audio.ambient` | Play ambient track | | `@audio.stop` | Stop a track | | `@prompt.directive.` | Inject/remove directive | | `@prompt.entry.` | Toggle entry visibility | | `@prompt.context` | One-shot context message | | `@rules.disabled.` | Toggle rule enabled/disabled | | `@ui.notification` | Show toast notification | | `@ai.request` | Trigger AI generation | | `@ai.context` | Add context for next AI message | ## Events Generated Each Turn After the AI responds, these events are emitted and checked against all rules/reactions: 1. `message:user` — Player's message (with content for keyword matching) 2. `message:ai` — AI's response (with content for keyword matching) 3. `turn:complete` — Turn finished (with turn count) 4. `state:changed` — One per variable that changed (with variableId, oldValue, newValue) ---
# Macros Macros are `{{placeholder}}` tokens in entry content that get replaced at runtime. They work in all entry types across all sections. ## Built-In Macros ### Identity | Macro | Replaced With | |-------|--------------| | `{{char}}` | Character name (from world settings) | | `{{user}}` | Player name (persona-aware: active persona name → account username → "Player") | ### Persona | Macro | Replaced With | |-------|--------------| | `{{persona}}` | All persona fields combined | | `{{persona_name}}` | Active persona's name | | `{{persona_appearance}}` | Active persona's appearance description | | `{{persona_personality}}` | Active persona's personality description | | `{{persona_backstory}}` | Active persona's backstory | ### Time | Macro | Replaced With | |-------|--------------| | `{{time}}` | Current time (HH:MM format) | | `{{date}}` | Current date (human-readable) | | `{{weekday}}` | Current day of the week | | `{{isodate}}` | ISO 8601 date | | `{{isotime}}` | ISO 8601 time | | `{{idle}}` | Time since last player message (human-readable, e.g., "5 minutes") | ### Game State | Macro | Replaced With | |-------|--------------| | `{{turnCount}}` | Current turn number | | `{{model}}` | Current LLM model ID | | `{{lastMessage}}` | Content of the most recent message | | `{{lastUserMessage}}` | Content of the most recent player message | | `{{lastCharMessage}}` | Content of the most recent AI message | ### Randomization | Macro | Replaced With | |-------|--------------| | `{{random::a::b::c}}` | Random selection from the options (re-rolls each turn) | | `{{pick::a::b::c}}` | Deterministic selection (seeded, stable within a turn) | | `{{roll::NdS}}` | Dice roll result, e.g., `{{roll::2d6+1}}` | ### Utility | Macro | Replaced With | |-------|--------------| | `{{// comment}}` | Removed (invisible comments in entries) | | `{{trim}}` | Collapses surrounding whitespace | ### Variable Macros Any variable ID works as a macro: ``` {{health}} → current value of the "health" variable {{location}} → current value of the "location" variable {{inventory}} → JSON.stringify of the "inventory" variable ``` If the macro doesn't match any built-in name or variable ID, it's left as-is (literal `{{unknown}}`).
--- # Custom UI (rootComponent) The `rootComponent` is a virtual filesystem of React/TSX files that run in a sandboxed iframe. It provides the world's visual layer and has full access to game state, chat control, session management, AI completions, and audio. ## rootComponent Schema ```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" } } ``` ## Entry Point `index.tsx` must export a default component: ```tsx export default function App() { return ; } ``` With custom message bubbles: ```tsx import Bubble from "./bubble"; export default function App() { return ; } ``` Full app mode: ```tsx export default function App() { var api = useYumina(); return (
HP: {api.variables.health}
); } ``` ## useYumina() — Complete API Reference ### State Reads ```typescript interface SandboxedYuminaAPI { // Game state variables: Record; globalVariables: Record; // 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; 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; bgmVolume: number; sfxVolume: number; } ``` ### Chat Actions ```typescript sendMessage(text: string): void; editMessage(messageId: string, content: string): Promise; deleteMessage(messageId: string): Promise; regenerateMessage(messageId: string): void; continueLastMessage(): void; stopGeneration(): void; restartChat(): void; swipeMessage(messageId: string, direction: "left" | "right"): Promise>; setComposerDraft(text: string): void; clearPendingChoices(): void; ``` ### Session Management ```typescript revertToMessage(messageId: string): Promise; branchFromMessage(messageId: string): Promise; getBranchContext(): Promise; createSession(worldId: string): Promise; deleteSession(sessionId: string): Promise; listSessions(worldId: string): Promise>>; navigate(path: string): void; ``` ### Checkpoints ```typescript saveCheckpoint(): Promise; loadCheckpoints(): Promise; restoreCheckpoint(checkpointId: string): Promise; deleteCheckpoint(checkpointId: string): Promise; ``` ### AI Completions ```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; ``` Make raw LLM calls with optional streaming and lorebook injection. Use for NPC generators, dynamic descriptions, hint systems, or any AI logic outside the main chat flow. ### Game Actions ```typescript setVariable(id: string, value: unknown, options?: { scope?: string; targetUserId?: string; }): void; executeAction(actionId: string): void; injectContext(message: string, options?: { role?: "system" | "user" }): void; ``` ### Audio ```typescript playAudio(trackId: string, opts?: { volume?: number; fadeDuration?: number; chainTo?: string; maxDuration?: number; duckBgm?: boolean; }): void; stopAudio(trackId?: string, fadeDuration?: number): void; setAudioVolume(type: "bgm" | "sfx", volume: number): void; getAudioVolume(type: "bgm" | "sfx"): number; ``` ### Storage (World-Scoped, Persistent) ```typescript storage.get(key: string): Promise; storage.set(key: string, value: string): Promise; storage.remove(key: string): Promise; ``` ### UI Controls ```typescript toggleImmersive(): 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; ``` ### Model Selection ```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; }>; ``` ## Built-In Components Available as globals — no imports needed. Import statements are silently stripped at compile time, so both styles work, but the components are injected into scope automatically: ```tsx // These are already in scope — just use them directly: // Chat, MessageList, MessageInput, ChatCanvas, // ModelPickerModal, ModelTrigger, useAssetFont, Icons // Import statements are harmless (stripped at compile time) but unnecessary: // import { Chat } from "yumina/Chat"; ← works but not needed ``` ### 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 | null; variables: Record; 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 | null; stateSnapshot?: Record | null; swipes?: Array<{ content: string; stateSnapshot?: Record | 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[]; } ``` ## Global API (Non-React) ```js window.yumina // Same API as useYumina() window.yumina.onChange(cb) // Subscribe to state changes, returns unsubscribe fn window.yumina.offChange(cb) // Unsubscribe // Also dispatches "yumina:statechange" event on window ``` ## Sandbox Environment React is available globally (no import needed). Use `React.useState`, `React.useEffect`, etc. ### Restrictions - No `fetch` / `XMLHttpRequest` - No direct `localStorage` / `sessionStorage` (use `storage.*` API instead) - No `window.location` manipulation (use `navigate()`) - No `window.parent` access - No `eval` / `new Function` - No cookie access ### Compatibility Shims Old world code without the SDK can still use: - `fetch('/api/*')` → proxied through parent with credentials - `localStorage` / `sessionStorage` → world-scoped, proxied to parent - `navigator.clipboard.writeText()` → proxied - `window.location` → synthetic object ### Styling Tailwind CSS is fully available in the sandbox — use any utility classes (`flex`, `gap-4`, `text-white`, `bg-[#1a1a2e]`, etc.). Inline styles also work: ```tsx // Tailwind classes (preferred)
// Inline styles (also fine) var style = { background: "#1a1a2e", color: "#e0e0e0", fontFamily: '"Noto Serif SC", serif', padding: "16px", }; ``` ### Multi-File Structure ```tsx // index.tsx import StatusPanel from "./status-panel"; import MapView from "./map-view"; export default function App() { return ( <> ); } ``` --- # Audio Audio tracks provide background music, sound effects, and ambient sounds. They can be triggered by AI directives, rules, or automatic playlists. ## AudioTrack Schema ```json { "id": "bgm-tavern", "name": "Tavern Theme", "type": "bgm", "url": "@asset:uuid-here", "loop": true, "volume": 0.4, "fadeIn": 2, "fadeOut": 1.5, "maxDuration": null } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique kebab-case ID | | `name` | string | Display name | | `type` | `"bgm"` \| `"sfx"` \| `"ambient"` | Track type | | `url` | string | Audio source — `@asset:{id}` or direct URL | | `loop` | boolean | Whether to loop (typical: true for BGM/ambient, false for SFX) | | `volume` | number | 0.0 to 1.0 | | `fadeIn` | number \| null | Fade-in duration in seconds | | `fadeOut` | number \| null | Fade-out duration in seconds | | `maxDuration` | number \| null | Auto-stop after N seconds | ## AI Audio Directives The AI triggers audio through bracket directives: ``` [audio: bgm-tavern play] [audio: sfx-sword play] [audio: bgm-tavern stop] [audio: bgm-battle crossfade 2.5] [audio: ambient-rain volume 0.3] [audio: sfx-magic play chain:bgm-ambient] ``` | Action | Description | |--------|-------------| | `play` | Start playing the track | | `stop` | Stop the track | | `crossfade ` | Crossfade from current BGM to this track | | `volume <0-1>` | Change track volume | | `play chain:` | Play this track, then auto-play the next | ## BGM Playlist Auto-play a sequence of BGM tracks: ```json { "bgmPlaylist": { "tracks": ["bgm-explore-1", "bgm-explore-2", "bgm-explore-3"], "playMode": "shuffle", "gapSeconds": 0, "autoPlay": true, "waitForFirstMessage": true } } ``` | Field | Type | Description | |-------|------|-------------| | `tracks` | string[] | Array of track IDs to cycle through | | `playMode` | `"loop"` \| `"shuffle"` \| `"sequential"` | Playback order mode | | `gapSeconds` | number | Silence between tracks (default: 0, max: 30) | | `autoPlay` | boolean | Start playing automatically (default: true) | | `waitForFirstMessage` | boolean | Don't start until the player sends their first message | ## Conditional BGM Play specific tracks when conditions are met. Supports variable checks, keyword matching, turn count, and session start triggers. ```json { "conditionalBGM": [ { "id": "combat-music", "name": "Combat Music", "targetTrackId": "bgm-battle", "triggerType": "variable", "conditions": [ { "variableId": "in-combat", "operator": "eq", "value": true } ], "conditionLogic": "all", "priority": 10, "fadeInDuration": 1, "fadeOutDuration": 2, "stopPreviousBGM": true, "fallback": "default" } ] } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier | | `name` | string | Display name | | `targetTrackId` | string | Track to play when triggered | | `triggerType` | `"variable"` \| `"ai-keyword"` \| `"keyword"` \| `"turn-count"` \| `"session-start"` | What triggers the conditional BGM | | `conditions` | Condition[] | Variable conditions to check | | `conditionLogic` | `"all"` \| `"any"` | How multiple conditions combine (default: `"all"`) | | `keywords` | string[] | Keywords to match (for `keyword` / `ai-keyword` triggers) | | `matchWholeWords` | boolean | Match whole words only (for keyword triggers) | | `atTurn` | number | Fire at specific turn (for `turn-count` trigger) | | `everyNTurns` | number | Fire every N turns (for `turn-count` trigger) | | `priority` | number | Higher priority wins when multiple triggers match | | `fadeInDuration` | number | Fade-in duration in seconds | | `fadeOutDuration` | number | Fade-out duration in seconds | | `stopPreviousBGM` | boolean | Stop the currently playing BGM first | | `fallback` | `"default"` \| `"previous"` \| trackId string | What to play when the condition becomes false | Condition operators use short form: `eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `contains`. When the condition becomes true, the track starts. When false, it stops (with fade-out if configured) and falls back according to the `fallback` setting.