speakid-story-builder 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/components/StoryBuilderGame.d.ts +9 -0
- package/dist/components/StoryBuilderGame.d.ts.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/data/questions.d.ts +4 -0
- package/dist/data/questions.d.ts.map +1 -0
- package/dist/data/stories.d.ts +8 -0
- package/dist/data/stories.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/speakid-story-builder.cjs.js +2 -0
- package/dist/speakid-story-builder.cjs.js.map +1 -0
- package/dist/speakid-story-builder.es.js +324 -0
- package/dist/speakid-story-builder.es.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/getQuestionsForStory.d.ts +8 -0
- package/dist/utils/getQuestionsForStory.d.ts.map +1 -0
- package/dist/utils/renderText.d.ts +7 -0
- package/dist/utils/renderText.d.ts.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Story Builder
|
|
2
|
+
|
|
3
|
+
Interactive story game for English practice (SPEAKID). The story is chosen first; it defines which questions the student answers. Answers are then woven into a comic-style story.
|
|
4
|
+
|
|
5
|
+
## Flow
|
|
6
|
+
|
|
7
|
+
1. **Lobby** — Play
|
|
8
|
+
2. **Play** — Pick a random story → story provides `inputKeys` and `comprehensionQuestions` → show **Questions** (5 inputs, first is always "What is your name?")
|
|
9
|
+
3. **Story** — Slides with substituted answers; prev/next navigation
|
|
10
|
+
4. **Comprehension** — Questions about the story (if any)
|
|
11
|
+
5. **End** — Try again (new story) | Return to lobby
|
|
12
|
+
|
|
13
|
+
## Tech
|
|
14
|
+
|
|
15
|
+
- React 18+, TypeScript, Vite
|
|
16
|
+
- Layout: 1280×720 base, scaled to viewport; portrait mobile shows "Please rotate your device"
|
|
17
|
+
- CSS Modules, all selectors under `.root` for NPM host isolation
|
|
18
|
+
|
|
19
|
+
## Dev
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install
|
|
23
|
+
npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Open http://localhost:5174
|
|
27
|
+
|
|
28
|
+
## Build (library)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm run build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Output: `dist/` (ES + CJS + types + style.css).
|
|
35
|
+
|
|
36
|
+
## Use as NPM
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { StoryBuilder } from "speakid-story-builder";
|
|
40
|
+
import "speakid-story-builder/style.css";
|
|
41
|
+
|
|
42
|
+
<StoryBuilder
|
|
43
|
+
questions={questions}
|
|
44
|
+
stories={stories}
|
|
45
|
+
onEvent={(e) => console.log(e)}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Data
|
|
50
|
+
|
|
51
|
+
- **Question bank**: `Question { id, answerKey, text, placeholder? }`. Stories reference keys (e.g. `name`, `goal`, `problem`).
|
|
52
|
+
- **Stories**: `Story { id, slides, inputKeys, comprehensionQuestions? }`. `inputKeys` is the list of answer keys needed (e.g. `["name", "goal", "problem", "food", "place"]`); first should be `name`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Question, Story, GameEvent, StoryBuilderConfig } from "../types";
|
|
2
|
+
export interface StoryBuilderGameProps {
|
|
3
|
+
config?: StoryBuilderConfig;
|
|
4
|
+
questions: Question[];
|
|
5
|
+
stories: Story[];
|
|
6
|
+
onEvent?: (event: GameEvent) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function StoryBuilder({ config, questions: questionBank, stories, onEvent, }: StoryBuilderGameProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
//# sourceMappingURL=StoryBuilderGame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StoryBuilderGame.d.ts","sourceRoot":"","sources":["../../src/components/StoryBuilderGame.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAkB,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAkB/F,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,SAAS,EAAE,YAAY,EACvB,OAAO,EACP,OAAO,GACR,EAAE,qBAAqB,kDAwTvB"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,2BAA2B,IAAI,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"questions.d.ts","sourceRoot":"","sources":["../../src/data/questions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,gFAAgF;AAChF,eAAO,MAAM,SAAS,EAAE,QAAQ,EA4D/B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Story } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* All stories. Images live in public/stories/{story.id}/ (e.g. slide1.png).
|
|
4
|
+
* Add a new story: create folder public/stories/{id}/, add slides and imageUrls, add questions for inputKeys.
|
|
5
|
+
* See docs/ADDING_STORIES.md.
|
|
6
|
+
*/
|
|
7
|
+
export declare const stories: Story[];
|
|
8
|
+
//# sourceMappingURL=stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stories.d.ts","sourceRoot":"","sources":["../../src/data/stories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,KAAK,EA2Q1B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { StoryBuilder } from "./components/StoryBuilderGame";
|
|
2
|
+
export type { StoryBuilderGameProps as StoryBuilderProps } from "./components/StoryBuilderGame";
|
|
3
|
+
export type { Question, Story, Slide, ComprehensionQuestion, Answers, Stage, GameEvent, StoryBuilderConfig, } from "./types";
|
|
4
|
+
export { getQuestionsForStory } from "./utils/getQuestionsForStory";
|
|
5
|
+
export { renderText } from "./utils/renderText";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7D,YAAY,EAAE,qBAAqB,IAAI,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAChG,YAAY,EACV,QAAQ,EACR,KAAK,EACL,KAAK,EACL,qBAAqB,EACrB,OAAO,EACP,KAAK,EACL,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),c=require("react");function x(d,l){const b=new Map(d.map(o=>[o.id,o]));if(l.questionIds&&l.questionIds.length===l.inputKeys.length)return l.questionIds.map(o=>{const u=b.get(o);if(!u)throw new Error(`Question not found: ${o}`);return u});const i=new Map;for(const o of d){const u=i.get(o.answerKey)??[];u.push(o),i.set(o.answerKey,u)}return l.inputKeys.map(o=>{const u=i.get(o);if(!u||u.length===0)throw new Error(`No question found for answerKey: ${o}`);return u[Math.floor(Math.random()*u.length)]})}const K={name:"the hero",goal:"a dream",problem:"doubts",place:"far away",food:"treasure",thing:"courage",pet:"a friend",father:"Dad",mum:"Mum",neighbour:"Mrs Brown",friend:"a friend",profession:"a hero",enemy:"the dragon",hobby:"dancing",book:"about adventures",subject:"nature",librarian:"the librarian",genre:"adventure",style:"ballet"},P=new Set(["name","father","mum","dad","friend","enemy","groom","pet","coach","teacher","librarian","neighbour"]);function G(d,l){const b=l.trim();return b.length===0?b:P.has(d)?b.charAt(0).toUpperCase()+b.slice(1).toLowerCase():b.toLowerCase()}function B(d,l){return d.replace(/\{\{(\w+)\}\}/g,(b,i)=>{const o=l[i];return o!=null&&o!==""?G(i,String(o)):K[i]??"something"})}const q="_root_j1p5w_1",Q="_container_j1p5w_76",H="_lobbyScreen_j1p5w_89",M="_lobbyContent_j1p5w_96",U="_storyList_j1p5w_103",O="_button_j1p5w_116",D="_storySelectBack_j1p5w_122",R="_questionsScreen_j1p5w_126",z="_headline1_j1p5w_134",X="_headline2_j1p5w_142",Z="_inputHint_j1p5w_150",Y="_buttonLarge_j1p5w_186",V="_inputGroup_j1p5w_192",W="_slideContainer_j1p5w_224",J="_slideFrame_j1p5w_234",E="_slideImage_j1p5w_242",ee="_slideLayers_j1p5w_252",se="_slideLayer_j1p5w_252",te="_slideLayerCharacter_j1p5w_272",ne="_slideSpeechBubble_j1p5w_287",ie="_slideSpeechBubbleCenter_j1p5w_305",oe="_slideSpeechBubbleTopSmall_j1p5w_316",le="_slideSpeechBubbleBottom_j1p5w_321",re="_slideNav_j1p5w_333",ae="_slideNavBtn_j1p5w_350",ce="_comprehensionSlide_j1p5w_386",de="_comprehensionSlideImage_j1p5w_393",ue="_comprehensionBubble_j1p5w_416",be="_comprehensionBubbleList_j1p5w_444",he="_comprehensionActionsOnly_j1p5w_469",pe="_endActions_j1p5w_531",e={root:q,container:Q,lobbyScreen:H,lobbyContent:M,storyList:U,button:O,storySelectBack:D,questionsScreen:R,headline1:z,headline2:X,inputHint:Z,buttonLarge:Y,inputGroup:V,slideContainer:W,slideFrame:J,slideImage:E,slideLayers:ee,slideLayer:se,slideLayerCharacter:te,slideSpeechBubble:ne,slideSpeechBubbleCenter:ie,slideSpeechBubbleTopSmall:oe,slideSpeechBubbleBottom:le,slideNav:re,slideNavBtn:ae,comprehensionSlide:ce,comprehensionSlideImage:de,comprehensionBubble:ue,comprehensionBubbleList:be,comprehensionActionsOnly:he,endActions:pe},A=40,me=/[^a-zA-Z ]/g;function _e(d){return d.replace(me,"").slice(0,A)}function ye(d){const l=d.trim();return l?l.charAt(0).toUpperCase()+l.slice(1):d}function je({config:d,questions:l,stories:b,onEvent:i}){var C,L;const[o,u]=c.useState("lobby"),[r,N]=c.useState(null),[y,S]=c.useState([]),[g,j]=c.useState({}),[p,_]=c.useState(0),a=c.useCallback(s=>{u(s),i==null||i({type:"STAGE_CHANGE",stage:s})},[i]),I=c.useCallback((s,n)=>{j(h=>({...h,[s]:n}));const m=y.find(h=>h.answerKey===s);m&&(i==null||i({type:"ANSWER_SUBMIT",questionId:m.id,value:n}))},[y,i]),$=c.useCallback(()=>{var n;if(!r)return;const s=r.slides.length;if(p<s-1){const m=p+1;_(m),i==null||i({type:"SLIDE_CHANGE",index:m})}else(n=r.comprehensionQuestions)!=null&&n.length?a("comprehension"):a("end")},[r,p,i,a]),v=c.useCallback(()=>{p>0&&(_(p-1),i==null||i({type:"SLIDE_CHANGE",index:p-1}))},[p,i]),T=c.useCallback(s=>{const n=x(l,s);N(s),S(n),j({}),_(0),a("questions")},[l,a]),k=c.useCallback(()=>{a("story")},[a]),w=c.useCallback(()=>{if(!r)return;const s=x(l,r);S(s),j({}),_(0),a("questions")},[r,l,a]),f=c.useCallback(()=>{N(null),S([]),j({}),_(0),a("lobby")},[a]);if(o==="lobby")return t.jsx("div",{className:`${e.root} ${e.lobbyScreen}`,children:t.jsx("div",{className:e.container,children:t.jsxs("div",{className:e.lobbyContent,children:[t.jsx("h1",{className:e.headline1,children:"Story Builder"}),t.jsx("p",{className:e.headline2,children:"Practice English with your own story"}),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:()=>a("storySelect"),children:"Play"})]})})});if(o==="storySelect")return t.jsx("div",{className:`${e.root} ${e.questionsScreen}`,children:t.jsxs("div",{className:e.container,children:[t.jsx("h2",{className:e.headline2,children:"Choose a story"}),t.jsx("div",{className:e.storyList,children:b.map(s=>t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:()=>T(s),children:s.title??s.id},s.id))}),t.jsx("button",{type:"button",className:`${e.button} ${e.storySelectBack}`,onClick:()=>a("lobby"),children:"Back"})]})});if(o==="questions"){const s=y.every(n=>(g[n.answerKey]??"").trim().length>0);return t.jsx("div",{className:`${e.root} ${e.questionsScreen}`,children:t.jsxs("div",{className:e.container,children:[t.jsx("h2",{className:e.headline2,children:"Answer a few questions"}),t.jsx("p",{className:e.inputHint,children:"Only Latin letters (A–Z) and spaces, max 40 characters per field."}),y.map(n=>t.jsxs("div",{className:e.inputGroup,children:[t.jsx("label",{htmlFor:n.id,children:n.text}),t.jsx("input",{id:n.id,type:"text",maxLength:A,autoComplete:"off",placeholder:n.placeholder,value:g[n.answerKey]??"",onChange:m=>{let h=_e(m.target.value);n.answerKey==="subject"&&(h=ye(h)),I(n.answerKey,h)}})]},n.id)),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:k,disabled:!s,children:"Continue"})]})})}if(o==="story"&&r){const s=r.slides[p];let n=e.slideSpeechBubble;return(s==null?void 0:s.speechBubblePosition)==="center"||(s==null?void 0:s.speechBubblePosition)===void 0&&(s==null?void 0:s.id)==="s6"&&r.id==="hero-1"?n=`${e.slideSpeechBubble} ${e.slideSpeechBubbleCenter}`:(s==null?void 0:s.speechBubblePosition)==="bottom"||(s==null?void 0:s.speechBubblePosition)===void 0&&(s==null?void 0:s.id)==="s7"&&r.id==="hero-1"?n=`${e.slideSpeechBubble} ${e.slideSpeechBubbleBottom}`:r.id==="hero-8"&&(s==null?void 0:s.id)==="s6"&&(n=`${e.slideSpeechBubble} ${e.slideSpeechBubbleTopSmall}`),t.jsx("div",{className:e.root,children:t.jsx("div",{className:e.slideContainer,children:t.jsxs("div",{className:e.slideFrame,children:[(C=s==null?void 0:s.imageLayers)!=null&&C.length?t.jsx("div",{className:e.slideLayers,children:s.imageLayers.map((m,h)=>{const F=h===s.imageLayers.length-1;return t.jsx("img",{src:m,alt:"",className:F?e.slideLayerCharacter:e.slideLayer},h)})}):s!=null&&s.imageUrl?t.jsx("img",{src:s.imageUrl,alt:"",className:e.slideImage}):t.jsx("div",{className:e.slideImage,"aria-hidden":!0}),t.jsx("div",{className:n,role:"dialog","aria-label":"Hero says",children:s?B(s.text,g):""}),t.jsxs("div",{className:e.slideNav,children:[t.jsx("button",{type:"button",className:e.slideNavBtn,onClick:v,disabled:p===0,"aria-label":"Previous slide",children:t.jsx("img",{src:"/slide-prev.png",alt:""})}),t.jsx("button",{type:"button",className:e.slideNavBtn,onClick:$,"aria-label":"Next slide",children:t.jsx("img",{src:"/slide-next.png",alt:""})})]})]})})})}if(o==="comprehension"&&((L=r==null?void 0:r.comprehensionQuestions)!=null&&L.length)){const s=r.comprehensionImageUrl;return t.jsx("div",{className:e.root,children:t.jsxs("div",{className:e.comprehensionSlide,children:[s?t.jsx("img",{src:s,alt:"",className:e.comprehensionSlideImage}):t.jsx("div",{className:e.comprehensionSlideImage,"aria-hidden":!0}),!r.comprehensionQuestionsOnImage&&t.jsx("div",{className:e.comprehensionBubble,role:"dialog","aria-label":"Comprehension questions",children:t.jsx("ul",{className:e.comprehensionBubbleList,children:r.comprehensionQuestions.map(n=>t.jsx("li",{children:n.text},n.id))})}),t.jsxs("div",{className:e.comprehensionActionsOnly,children:[t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:w,children:"PLAY AGAIN"}),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:f,children:"EXIT"})]})]})})}return o==="end"?t.jsx("div",{className:e.root,children:t.jsxs("div",{className:e.container,children:[t.jsx("h1",{className:e.headline1,children:"The End"}),t.jsxs("div",{className:e.endActions,children:[t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:w,children:"Try again"}),t.jsx("button",{type:"button",className:`${e.button} ${e.buttonLarge}`,onClick:f,children:"Return to lobby"})]})]})}):null}exports.StoryBuilder=je;exports.getQuestionsForStory=x;exports.renderText=B;
|
|
2
|
+
//# sourceMappingURL=speakid-story-builder.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speakid-story-builder.cjs.js","sources":["../src/utils/getQuestionsForStory.ts","../src/utils/renderText.ts","../src/components/StoryBuilderGame.tsx"],"sourcesContent":["import type { Question, Story } from \"../types\";\n\n/**\n * For each key in story.inputKeys, pick one question:\n * - If story.questionIds is set (same length as inputKeys), use those question ids from the bank.\n * - Otherwise pick one random question per answerKey.\n */\nexport function getQuestionsForStory(\n questionBank: Question[],\n story: Story\n): Question[] {\n const byId = new Map(questionBank.map((q) => [q.id, q]));\n\n if (story.questionIds && story.questionIds.length === story.inputKeys.length) {\n return story.questionIds.map((id) => {\n const q = byId.get(id);\n if (!q) throw new Error(`Question not found: ${id}`);\n return q;\n });\n }\n\n const byKey = new Map<string, Question[]>();\n for (const q of questionBank) {\n const list = byKey.get(q.answerKey) ?? [];\n list.push(q);\n byKey.set(q.answerKey, list);\n }\n\n return story.inputKeys.map((key) => {\n const list = byKey.get(key);\n if (!list || list.length === 0) {\n throw new Error(`No question found for answerKey: ${key}`);\n }\n return list[Math.floor(Math.random() * list.length)];\n });\n}\n","/** Нейтральные подстановки, чтобы пользователь никогда не видел «дырки» в тексте */\nconst DEFAULT_FALLBACKS: Record<string, string> = {\n name: \"the hero\",\n goal: \"a dream\",\n problem: \"doubts\",\n place: \"far away\",\n food: \"treasure\",\n thing: \"courage\",\n pet: \"a friend\",\n father: \"Dad\",\n mum: \"Mum\",\n neighbour: \"Mrs Brown\",\n friend: \"a friend\",\n profession: \"a hero\",\n enemy: \"the dragon\",\n hobby: \"dancing\",\n book: \"about adventures\",\n subject: \"nature\",\n librarian: \"the librarian\",\n genre: \"adventure\",\n style: \"ballet\",\n};\n\n/** Имена и обращения — всегда с большой буквы; профессия, еда, хобби — lower case */\nconst CAPITALIZE_KEYS = new Set([\n \"name\",\n \"father\",\n \"mum\",\n \"dad\",\n \"friend\",\n \"enemy\",\n \"groom\",\n \"pet\",\n \"coach\",\n \"teacher\",\n \"librarian\",\n \"neighbour\",\n]);\n\nfunction formatValue(key: string, raw: string): string {\n const s = raw.trim();\n if (s.length === 0) return s;\n if (CAPITALIZE_KEYS.has(key)) {\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n }\n return s.toLowerCase();\n}\n\n/**\n * Replaces placeholders {{key}} in text with values from answers.\n * name, father, friend, enemy → first letter uppercase; profession, food → lowercase.\n * Missing key → neutral fallback (never empty), so the sentence always reads well.\n */\nexport function renderText(text: string, answers: Record<string, string>): string {\n return text.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n const value = answers[key];\n if (value != null && value !== \"\") return formatValue(key, String(value));\n return DEFAULT_FALLBACKS[key] ?? \"something\";\n });\n}\n","import React, { useState, useCallback } from \"react\";\nimport type { Question, Story, Answers, Stage, GameEvent, StoryBuilderConfig } from \"../types\";\nimport { getQuestionsForStory } from \"../utils/getQuestionsForStory\";\nimport { renderText } from \"../utils/renderText\";\nimport cssStyles from \"./StoryBuilder.module.css\";\n\nconst INPUT_MAX_LENGTH = 40;\nconst LATIN_AND_SPACE_REGEX = /[^a-zA-Z ]/g;\n\nfunction sanitizeInput(value: string): string {\n return value.replace(LATIN_AND_SPACE_REGEX, \"\").slice(0, INPUT_MAX_LENGTH);\n}\n\nfunction capitalizeFirstLetter(value: string): string {\n const t = value.trim();\n if (!t) return value;\n return t.charAt(0).toUpperCase() + t.slice(1);\n}\n\nexport interface StoryBuilderGameProps {\n config?: StoryBuilderConfig;\n questions: Question[];\n stories: Story[];\n onEvent?: (event: GameEvent) => void;\n}\n\nexport function StoryBuilder({\n config,\n questions: questionBank,\n stories,\n onEvent,\n}: StoryBuilderGameProps) {\n const [stage, setStageState] = useState<Stage>(\"lobby\");\n const [currentStory, setCurrentStory] = useState<Story | null>(null);\n const [currentQuestions, setCurrentQuestions] = useState<Question[]>([]);\n const [answers, setAnswersState] = useState<Answers>({});\n const [slideIndex, setSlideIndexState] = useState(0);\n\n const goToStage = useCallback(\n (next: Stage) => {\n setStageState(next);\n onEvent?.({ type: \"STAGE_CHANGE\", stage: next });\n },\n [onEvent]\n );\n\n const setAnswer = useCallback(\n (answerKey: string, value: string) => {\n setAnswersState((prev) => ({ ...prev, [answerKey]: value }));\n const q = currentQuestions.find((c) => c.answerKey === answerKey);\n if (q) onEvent?.({ type: \"ANSWER_SUBMIT\", questionId: q.id, value });\n },\n [currentQuestions, onEvent]\n );\n\n const nextSlide = useCallback(() => {\n if (!currentStory) return;\n const contentLength = currentStory.slides.length;\n if (slideIndex < contentLength - 1) {\n const next = slideIndex + 1;\n setSlideIndexState(next);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: next });\n } else {\n if (currentStory.comprehensionQuestions?.length) {\n goToStage(\"comprehension\");\n } else {\n goToStage(\"end\");\n }\n }\n }, [currentStory, slideIndex, onEvent, goToStage]);\n\n const prevSlide = useCallback(() => {\n if (slideIndex > 0) {\n setSlideIndexState(slideIndex - 1);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: slideIndex - 1 });\n }\n }, [slideIndex, onEvent]);\n\n const handleSelectStory = useCallback(\n (story: Story) => {\n const questionsForStory = getQuestionsForStory(questionBank, story);\n setCurrentStory(story);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n },\n [questionBank, goToStage]\n );\n\n const handleQuestionsSubmit = useCallback(() => {\n goToStage(\"story\");\n }, [goToStage]);\n\n const handleTryAgain = useCallback(() => {\n if (!currentStory) return;\n const questionsForStory = getQuestionsForStory(questionBank, currentStory);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n }, [currentStory, questionBank, goToStage]);\n\n const handleReturnToLobby = useCallback(() => {\n setCurrentStory(null);\n setCurrentQuestions([]);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"lobby\");\n }, [goToStage]);\n\n if (stage === \"lobby\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.lobbyScreen}`}>\n <div className={cssStyles.container}>\n <div className={cssStyles.lobbyContent}>\n <h1 className={cssStyles.headline1}>Story Builder</h1>\n <p className={cssStyles.headline2}>Practice English with your own story</p>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => goToStage(\"storySelect\")}\n >\n Play\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"storySelect\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Choose a story</h2>\n <div className={cssStyles.storyList}>\n {stories.map((story) => (\n <button\n key={story.id}\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => handleSelectStory(story)}\n >\n {story.title ?? story.id}\n </button>\n ))}\n </div>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.storySelectBack}`}\n onClick={() => goToStage(\"lobby\")}\n >\n Back\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"questions\") {\n const allFilled = currentQuestions.every(\n (q) => (answers[q.answerKey] ?? \"\").trim().length > 0\n );\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Answer a few questions</h2>\n <p className={cssStyles.inputHint}>Only Latin letters (A–Z) and spaces, max 40 characters per field.</p>\n {currentQuestions.map((q) => (\n <div key={q.id} className={cssStyles.inputGroup}>\n <label htmlFor={q.id}>{q.text}</label>\n <input\n id={q.id}\n type=\"text\"\n maxLength={INPUT_MAX_LENGTH}\n autoComplete=\"off\"\n placeholder={q.placeholder}\n value={answers[q.answerKey] ?? \"\"}\n onChange={(e) => {\n let v = sanitizeInput(e.target.value);\n if (q.answerKey === \"subject\") v = capitalizeFirstLetter(v);\n setAnswer(q.answerKey, v);\n }}\n />\n </div>\n ))}\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleQuestionsSubmit}\n disabled={!allFilled}\n >\n Continue\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"story\" && currentStory) {\n const slide = currentStory.slides[slideIndex];\n let speechBubbleClass = cssStyles.slideSpeechBubble;\n\n if (\n slide?.speechBubblePosition === \"center\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s6\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleCenter}`;\n } else if (\n slide?.speechBubblePosition === \"bottom\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s7\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleBottom}`;\n } else if (currentStory.id === \"hero-8\" && slide?.id === \"s6\") {\n // Финальный кадр School Concert: пузырь в верхнем левом углу, меньше по ширине\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleTopSmall}`;\n }\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.slideContainer}>\n <div className={cssStyles.slideFrame}>\n {/* Внутри кадра: фон + герой (единая сцена) */}\n {slide?.imageLayers?.length ? (\n <div className={cssStyles.slideLayers}>\n {slide.imageLayers.map((url, i) => {\n const isLast = i === slide.imageLayers!.length - 1;\n return (\n <img\n key={i}\n src={url}\n alt=\"\"\n className={isLast ? cssStyles.slideLayerCharacter : cssStyles.slideLayer}\n />\n );\n })}\n </div>\n ) : slide?.imageUrl ? (\n <img src={slide.imageUrl} alt=\"\" className={cssStyles.slideImage} />\n ) : (\n <div className={cssStyles.slideImage} aria-hidden />\n )}\n {/* Реплика: по slide.speechBubblePosition, с особыми случаями для hero-1 и финального кадра hero-8 */}\n <div className={speechBubbleClass} role=\"dialog\" aria-label=\"Hero says\">\n {slide ? renderText(slide.text, answers) : \"\"}\n </div>\n {/* Навигация внутри рамки, вторичная */}\n <div className={cssStyles.slideNav}>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={prevSlide}\n disabled={slideIndex === 0}\n aria-label=\"Previous slide\"\n >\n <img src=\"/slide-prev.png\" alt=\"\" />\n </button>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={nextSlide}\n aria-label=\"Next slide\"\n >\n <img src=\"/slide-next.png\" alt=\"\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"comprehension\" && currentStory?.comprehensionQuestions?.length) {\n const compImageUrl = currentStory.comprehensionImageUrl;\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.comprehensionSlide}>\n {compImageUrl ? (\n <img src={compImageUrl} alt=\"\" className={cssStyles.comprehensionSlideImage} />\n ) : (\n <div className={cssStyles.comprehensionSlideImage} aria-hidden />\n )}\n {!currentStory.comprehensionQuestionsOnImage && (\n <div className={cssStyles.comprehensionBubble} role=\"dialog\" aria-label=\"Comprehension questions\">\n <ul className={cssStyles.comprehensionBubbleList}>\n {currentStory.comprehensionQuestions.map((q) => (\n <li key={q.id}>{q.text}</li>\n ))}\n </ul>\n </div>\n )}\n <div className={cssStyles.comprehensionActionsOnly}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n PLAY AGAIN\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n EXIT\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"end\") {\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.container}>\n <h1 className={cssStyles.headline1}>The End</h1>\n <div className={cssStyles.endActions}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n Try again\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n Return to lobby\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n return null;\n}\n"],"names":["getQuestionsForStory","questionBank","story","byId","q","id","byKey","list","key","DEFAULT_FALLBACKS","CAPITALIZE_KEYS","formatValue","raw","s","renderText","text","answers","_","value","INPUT_MAX_LENGTH","LATIN_AND_SPACE_REGEX","sanitizeInput","capitalizeFirstLetter","t","StoryBuilder","config","stories","onEvent","stage","setStageState","useState","currentStory","setCurrentStory","currentQuestions","setCurrentQuestions","setAnswersState","slideIndex","setSlideIndexState","goToStage","useCallback","next","setAnswer","answerKey","prev","c","nextSlide","contentLength","_a","prevSlide","handleSelectStory","questionsForStory","handleQuestionsSubmit","handleTryAgain","handleReturnToLobby","cssStyles","jsx","jsxs","allFilled","e","v","slide","speechBubbleClass","url","i","isLast","_b","compImageUrl"],"mappings":"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"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { jsx as n, jsxs as b } from "react/jsx-runtime";
|
|
2
|
+
import { useState as g, useCallback as _ } from "react";
|
|
3
|
+
function I(c, r) {
|
|
4
|
+
const u = new Map(c.map((o) => [o.id, o]));
|
|
5
|
+
if (r.questionIds && r.questionIds.length === r.inputKeys.length)
|
|
6
|
+
return r.questionIds.map((o) => {
|
|
7
|
+
const d = u.get(o);
|
|
8
|
+
if (!d) throw new Error(`Question not found: ${o}`);
|
|
9
|
+
return d;
|
|
10
|
+
});
|
|
11
|
+
const i = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const o of c) {
|
|
13
|
+
const d = i.get(o.answerKey) ?? [];
|
|
14
|
+
d.push(o), i.set(o.answerKey, d);
|
|
15
|
+
}
|
|
16
|
+
return r.inputKeys.map((o) => {
|
|
17
|
+
const d = i.get(o);
|
|
18
|
+
if (!d || d.length === 0)
|
|
19
|
+
throw new Error(`No question found for answerKey: ${o}`);
|
|
20
|
+
return d[Math.floor(Math.random() * d.length)];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const P = {
|
|
24
|
+
name: "the hero",
|
|
25
|
+
goal: "a dream",
|
|
26
|
+
problem: "doubts",
|
|
27
|
+
place: "far away",
|
|
28
|
+
food: "treasure",
|
|
29
|
+
thing: "courage",
|
|
30
|
+
pet: "a friend",
|
|
31
|
+
father: "Dad",
|
|
32
|
+
mum: "Mum",
|
|
33
|
+
neighbour: "Mrs Brown",
|
|
34
|
+
friend: "a friend",
|
|
35
|
+
profession: "a hero",
|
|
36
|
+
enemy: "the dragon",
|
|
37
|
+
hobby: "dancing",
|
|
38
|
+
book: "about adventures",
|
|
39
|
+
subject: "nature",
|
|
40
|
+
librarian: "the librarian",
|
|
41
|
+
genre: "adventure",
|
|
42
|
+
style: "ballet"
|
|
43
|
+
}, k = /* @__PURE__ */ new Set([
|
|
44
|
+
"name",
|
|
45
|
+
"father",
|
|
46
|
+
"mum",
|
|
47
|
+
"dad",
|
|
48
|
+
"friend",
|
|
49
|
+
"enemy",
|
|
50
|
+
"groom",
|
|
51
|
+
"pet",
|
|
52
|
+
"coach",
|
|
53
|
+
"teacher",
|
|
54
|
+
"librarian",
|
|
55
|
+
"neighbour"
|
|
56
|
+
]);
|
|
57
|
+
function H(c, r) {
|
|
58
|
+
const u = r.trim();
|
|
59
|
+
return u.length === 0 ? u : k.has(c) ? u.charAt(0).toUpperCase() + u.slice(1).toLowerCase() : u.toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
function Q(c, r) {
|
|
62
|
+
return c.replace(/\{\{(\w+)\}\}/g, (u, i) => {
|
|
63
|
+
const o = r[i];
|
|
64
|
+
return o != null && o !== "" ? H(i, String(o)) : P[i] ?? "something";
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const U = "_root_j1p5w_1", q = "_container_j1p5w_76", M = "_lobbyScreen_j1p5w_89", O = "_lobbyContent_j1p5w_96", D = "_storyList_j1p5w_103", R = "_button_j1p5w_116", z = "_storySelectBack_j1p5w_122", X = "_questionsScreen_j1p5w_126", Z = "_headline1_j1p5w_134", Y = "_headline2_j1p5w_142", V = "_inputHint_j1p5w_150", W = "_buttonLarge_j1p5w_186", J = "_inputGroup_j1p5w_192", E = "_slideContainer_j1p5w_224", ee = "_slideFrame_j1p5w_234", te = "_slideImage_j1p5w_242", ne = "_slideLayers_j1p5w_252", se = "_slideLayer_j1p5w_252", ie = "_slideLayerCharacter_j1p5w_272", oe = "_slideSpeechBubble_j1p5w_287", re = "_slideSpeechBubbleCenter_j1p5w_305", le = "_slideSpeechBubbleTopSmall_j1p5w_316", ae = "_slideSpeechBubbleBottom_j1p5w_321", ce = "_slideNav_j1p5w_333", de = "_slideNavBtn_j1p5w_350", ue = "_comprehensionSlide_j1p5w_386", he = "_comprehensionSlideImage_j1p5w_393", be = "_comprehensionBubble_j1p5w_416", pe = "_comprehensionBubbleList_j1p5w_444", me = "_comprehensionActionsOnly_j1p5w_469", _e = "_endActions_j1p5w_531", e = {
|
|
68
|
+
root: U,
|
|
69
|
+
container: q,
|
|
70
|
+
lobbyScreen: M,
|
|
71
|
+
lobbyContent: O,
|
|
72
|
+
storyList: D,
|
|
73
|
+
button: R,
|
|
74
|
+
storySelectBack: z,
|
|
75
|
+
questionsScreen: X,
|
|
76
|
+
headline1: Z,
|
|
77
|
+
headline2: Y,
|
|
78
|
+
inputHint: V,
|
|
79
|
+
buttonLarge: W,
|
|
80
|
+
inputGroup: J,
|
|
81
|
+
slideContainer: E,
|
|
82
|
+
slideFrame: ee,
|
|
83
|
+
slideImage: te,
|
|
84
|
+
slideLayers: ne,
|
|
85
|
+
slideLayer: se,
|
|
86
|
+
slideLayerCharacter: ie,
|
|
87
|
+
slideSpeechBubble: oe,
|
|
88
|
+
slideSpeechBubbleCenter: re,
|
|
89
|
+
slideSpeechBubbleTopSmall: le,
|
|
90
|
+
slideSpeechBubbleBottom: ae,
|
|
91
|
+
slideNav: ce,
|
|
92
|
+
slideNavBtn: de,
|
|
93
|
+
comprehensionSlide: ue,
|
|
94
|
+
comprehensionSlideImage: he,
|
|
95
|
+
comprehensionBubble: be,
|
|
96
|
+
comprehensionBubbleList: pe,
|
|
97
|
+
comprehensionActionsOnly: me,
|
|
98
|
+
endActions: _e
|
|
99
|
+
}, $ = 40, ye = /[^a-zA-Z ]/g;
|
|
100
|
+
function ge(c) {
|
|
101
|
+
return c.replace(ye, "").slice(0, $);
|
|
102
|
+
}
|
|
103
|
+
function Se(c) {
|
|
104
|
+
const r = c.trim();
|
|
105
|
+
return r ? r.charAt(0).toUpperCase() + r.slice(1) : c;
|
|
106
|
+
}
|
|
107
|
+
function fe({
|
|
108
|
+
config: c,
|
|
109
|
+
questions: r,
|
|
110
|
+
stories: u,
|
|
111
|
+
onEvent: i
|
|
112
|
+
}) {
|
|
113
|
+
var j, A;
|
|
114
|
+
const [o, d] = g("lobby"), [l, L] = g(null), [S, w] = g([]), [f, N] = g({}), [p, y] = g(0), a = _(
|
|
115
|
+
(t) => {
|
|
116
|
+
d(t), i == null || i({ type: "STAGE_CHANGE", stage: t });
|
|
117
|
+
},
|
|
118
|
+
[i]
|
|
119
|
+
), v = _(
|
|
120
|
+
(t, s) => {
|
|
121
|
+
N((h) => ({ ...h, [t]: s }));
|
|
122
|
+
const m = S.find((h) => h.answerKey === t);
|
|
123
|
+
m && (i == null || i({ type: "ANSWER_SUBMIT", questionId: m.id, value: s }));
|
|
124
|
+
},
|
|
125
|
+
[S, i]
|
|
126
|
+
), T = _(() => {
|
|
127
|
+
var s;
|
|
128
|
+
if (!l) return;
|
|
129
|
+
const t = l.slides.length;
|
|
130
|
+
if (p < t - 1) {
|
|
131
|
+
const m = p + 1;
|
|
132
|
+
y(m), i == null || i({ type: "SLIDE_CHANGE", index: m });
|
|
133
|
+
} else
|
|
134
|
+
(s = l.comprehensionQuestions) != null && s.length ? a("comprehension") : a("end");
|
|
135
|
+
}, [l, p, i, a]), x = _(() => {
|
|
136
|
+
p > 0 && (y(p - 1), i == null || i({ type: "SLIDE_CHANGE", index: p - 1 }));
|
|
137
|
+
}, [p, i]), F = _(
|
|
138
|
+
(t) => {
|
|
139
|
+
const s = I(r, t);
|
|
140
|
+
L(t), w(s), N({}), y(0), a("questions");
|
|
141
|
+
},
|
|
142
|
+
[r, a]
|
|
143
|
+
), K = _(() => {
|
|
144
|
+
a("story");
|
|
145
|
+
}, [a]), B = _(() => {
|
|
146
|
+
if (!l) return;
|
|
147
|
+
const t = I(r, l);
|
|
148
|
+
w(t), N({}), y(0), a("questions");
|
|
149
|
+
}, [l, r, a]), C = _(() => {
|
|
150
|
+
L(null), w([]), N({}), y(0), a("lobby");
|
|
151
|
+
}, [a]);
|
|
152
|
+
if (o === "lobby")
|
|
153
|
+
return /* @__PURE__ */ n("div", { className: `${e.root} ${e.lobbyScreen}`, children: /* @__PURE__ */ n("div", { className: e.container, children: /* @__PURE__ */ b("div", { className: e.lobbyContent, children: [
|
|
154
|
+
/* @__PURE__ */ n("h1", { className: e.headline1, children: "Story Builder" }),
|
|
155
|
+
/* @__PURE__ */ n("p", { className: e.headline2, children: "Practice English with your own story" }),
|
|
156
|
+
/* @__PURE__ */ n(
|
|
157
|
+
"button",
|
|
158
|
+
{
|
|
159
|
+
type: "button",
|
|
160
|
+
className: `${e.button} ${e.buttonLarge}`,
|
|
161
|
+
onClick: () => a("storySelect"),
|
|
162
|
+
children: "Play"
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
] }) }) });
|
|
166
|
+
if (o === "storySelect")
|
|
167
|
+
return /* @__PURE__ */ n("div", { className: `${e.root} ${e.questionsScreen}`, children: /* @__PURE__ */ b("div", { className: e.container, children: [
|
|
168
|
+
/* @__PURE__ */ n("h2", { className: e.headline2, children: "Choose a story" }),
|
|
169
|
+
/* @__PURE__ */ n("div", { className: e.storyList, children: u.map((t) => /* @__PURE__ */ n(
|
|
170
|
+
"button",
|
|
171
|
+
{
|
|
172
|
+
type: "button",
|
|
173
|
+
className: `${e.button} ${e.buttonLarge}`,
|
|
174
|
+
onClick: () => F(t),
|
|
175
|
+
children: t.title ?? t.id
|
|
176
|
+
},
|
|
177
|
+
t.id
|
|
178
|
+
)) }),
|
|
179
|
+
/* @__PURE__ */ n(
|
|
180
|
+
"button",
|
|
181
|
+
{
|
|
182
|
+
type: "button",
|
|
183
|
+
className: `${e.button} ${e.storySelectBack}`,
|
|
184
|
+
onClick: () => a("lobby"),
|
|
185
|
+
children: "Back"
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
] }) });
|
|
189
|
+
if (o === "questions") {
|
|
190
|
+
const t = S.every(
|
|
191
|
+
(s) => (f[s.answerKey] ?? "").trim().length > 0
|
|
192
|
+
);
|
|
193
|
+
return /* @__PURE__ */ n("div", { className: `${e.root} ${e.questionsScreen}`, children: /* @__PURE__ */ b("div", { className: e.container, children: [
|
|
194
|
+
/* @__PURE__ */ n("h2", { className: e.headline2, children: "Answer a few questions" }),
|
|
195
|
+
/* @__PURE__ */ n("p", { className: e.inputHint, children: "Only Latin letters (A–Z) and spaces, max 40 characters per field." }),
|
|
196
|
+
S.map((s) => /* @__PURE__ */ b("div", { className: e.inputGroup, children: [
|
|
197
|
+
/* @__PURE__ */ n("label", { htmlFor: s.id, children: s.text }),
|
|
198
|
+
/* @__PURE__ */ n(
|
|
199
|
+
"input",
|
|
200
|
+
{
|
|
201
|
+
id: s.id,
|
|
202
|
+
type: "text",
|
|
203
|
+
maxLength: $,
|
|
204
|
+
autoComplete: "off",
|
|
205
|
+
placeholder: s.placeholder,
|
|
206
|
+
value: f[s.answerKey] ?? "",
|
|
207
|
+
onChange: (m) => {
|
|
208
|
+
let h = ge(m.target.value);
|
|
209
|
+
s.answerKey === "subject" && (h = Se(h)), v(s.answerKey, h);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
] }, s.id)),
|
|
214
|
+
/* @__PURE__ */ n(
|
|
215
|
+
"button",
|
|
216
|
+
{
|
|
217
|
+
type: "button",
|
|
218
|
+
className: `${e.button} ${e.buttonLarge}`,
|
|
219
|
+
onClick: K,
|
|
220
|
+
disabled: !t,
|
|
221
|
+
children: "Continue"
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
] }) });
|
|
225
|
+
}
|
|
226
|
+
if (o === "story" && l) {
|
|
227
|
+
const t = l.slides[p];
|
|
228
|
+
let s = e.slideSpeechBubble;
|
|
229
|
+
return (t == null ? void 0 : t.speechBubblePosition) === "center" || (t == null ? void 0 : t.speechBubblePosition) === void 0 && (t == null ? void 0 : t.id) === "s6" && l.id === "hero-1" ? s = `${e.slideSpeechBubble} ${e.slideSpeechBubbleCenter}` : (t == null ? void 0 : t.speechBubblePosition) === "bottom" || (t == null ? void 0 : t.speechBubblePosition) === void 0 && (t == null ? void 0 : t.id) === "s7" && l.id === "hero-1" ? s = `${e.slideSpeechBubble} ${e.slideSpeechBubbleBottom}` : l.id === "hero-8" && (t == null ? void 0 : t.id) === "s6" && (s = `${e.slideSpeechBubble} ${e.slideSpeechBubbleTopSmall}`), /* @__PURE__ */ n("div", { className: e.root, children: /* @__PURE__ */ n("div", { className: e.slideContainer, children: /* @__PURE__ */ b("div", { className: e.slideFrame, children: [
|
|
230
|
+
(j = t == null ? void 0 : t.imageLayers) != null && j.length ? /* @__PURE__ */ n("div", { className: e.slideLayers, children: t.imageLayers.map((m, h) => {
|
|
231
|
+
const G = h === t.imageLayers.length - 1;
|
|
232
|
+
return /* @__PURE__ */ n(
|
|
233
|
+
"img",
|
|
234
|
+
{
|
|
235
|
+
src: m,
|
|
236
|
+
alt: "",
|
|
237
|
+
className: G ? e.slideLayerCharacter : e.slideLayer
|
|
238
|
+
},
|
|
239
|
+
h
|
|
240
|
+
);
|
|
241
|
+
}) }) : t != null && t.imageUrl ? /* @__PURE__ */ n("img", { src: t.imageUrl, alt: "", className: e.slideImage }) : /* @__PURE__ */ n("div", { className: e.slideImage, "aria-hidden": !0 }),
|
|
242
|
+
/* @__PURE__ */ n("div", { className: s, role: "dialog", "aria-label": "Hero says", children: t ? Q(t.text, f) : "" }),
|
|
243
|
+
/* @__PURE__ */ b("div", { className: e.slideNav, children: [
|
|
244
|
+
/* @__PURE__ */ n(
|
|
245
|
+
"button",
|
|
246
|
+
{
|
|
247
|
+
type: "button",
|
|
248
|
+
className: e.slideNavBtn,
|
|
249
|
+
onClick: x,
|
|
250
|
+
disabled: p === 0,
|
|
251
|
+
"aria-label": "Previous slide",
|
|
252
|
+
children: /* @__PURE__ */ n("img", { src: "/slide-prev.png", alt: "" })
|
|
253
|
+
}
|
|
254
|
+
),
|
|
255
|
+
/* @__PURE__ */ n(
|
|
256
|
+
"button",
|
|
257
|
+
{
|
|
258
|
+
type: "button",
|
|
259
|
+
className: e.slideNavBtn,
|
|
260
|
+
onClick: T,
|
|
261
|
+
"aria-label": "Next slide",
|
|
262
|
+
children: /* @__PURE__ */ n("img", { src: "/slide-next.png", alt: "" })
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
] })
|
|
266
|
+
] }) }) });
|
|
267
|
+
}
|
|
268
|
+
if (o === "comprehension" && ((A = l == null ? void 0 : l.comprehensionQuestions) != null && A.length)) {
|
|
269
|
+
const t = l.comprehensionImageUrl;
|
|
270
|
+
return /* @__PURE__ */ n("div", { className: e.root, children: /* @__PURE__ */ b("div", { className: e.comprehensionSlide, children: [
|
|
271
|
+
t ? /* @__PURE__ */ n("img", { src: t, alt: "", className: e.comprehensionSlideImage }) : /* @__PURE__ */ n("div", { className: e.comprehensionSlideImage, "aria-hidden": !0 }),
|
|
272
|
+
!l.comprehensionQuestionsOnImage && /* @__PURE__ */ n("div", { className: e.comprehensionBubble, role: "dialog", "aria-label": "Comprehension questions", children: /* @__PURE__ */ n("ul", { className: e.comprehensionBubbleList, children: l.comprehensionQuestions.map((s) => /* @__PURE__ */ n("li", { children: s.text }, s.id)) }) }),
|
|
273
|
+
/* @__PURE__ */ b("div", { className: e.comprehensionActionsOnly, children: [
|
|
274
|
+
/* @__PURE__ */ n(
|
|
275
|
+
"button",
|
|
276
|
+
{
|
|
277
|
+
type: "button",
|
|
278
|
+
className: `${e.button} ${e.buttonLarge}`,
|
|
279
|
+
onClick: B,
|
|
280
|
+
children: "PLAY AGAIN"
|
|
281
|
+
}
|
|
282
|
+
),
|
|
283
|
+
/* @__PURE__ */ n(
|
|
284
|
+
"button",
|
|
285
|
+
{
|
|
286
|
+
type: "button",
|
|
287
|
+
className: `${e.button} ${e.buttonLarge}`,
|
|
288
|
+
onClick: C,
|
|
289
|
+
children: "EXIT"
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
] })
|
|
293
|
+
] }) });
|
|
294
|
+
}
|
|
295
|
+
return o === "end" ? /* @__PURE__ */ n("div", { className: e.root, children: /* @__PURE__ */ b("div", { className: e.container, children: [
|
|
296
|
+
/* @__PURE__ */ n("h1", { className: e.headline1, children: "The End" }),
|
|
297
|
+
/* @__PURE__ */ b("div", { className: e.endActions, children: [
|
|
298
|
+
/* @__PURE__ */ n(
|
|
299
|
+
"button",
|
|
300
|
+
{
|
|
301
|
+
type: "button",
|
|
302
|
+
className: `${e.button} ${e.buttonLarge}`,
|
|
303
|
+
onClick: B,
|
|
304
|
+
children: "Try again"
|
|
305
|
+
}
|
|
306
|
+
),
|
|
307
|
+
/* @__PURE__ */ n(
|
|
308
|
+
"button",
|
|
309
|
+
{
|
|
310
|
+
type: "button",
|
|
311
|
+
className: `${e.button} ${e.buttonLarge}`,
|
|
312
|
+
onClick: C,
|
|
313
|
+
children: "Return to lobby"
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
] })
|
|
317
|
+
] }) }) : null;
|
|
318
|
+
}
|
|
319
|
+
export {
|
|
320
|
+
fe as StoryBuilder,
|
|
321
|
+
I as getQuestionsForStory,
|
|
322
|
+
Q as renderText
|
|
323
|
+
};
|
|
324
|
+
//# sourceMappingURL=speakid-story-builder.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speakid-story-builder.es.js","sources":["../src/utils/getQuestionsForStory.ts","../src/utils/renderText.ts","../src/components/StoryBuilderGame.tsx"],"sourcesContent":["import type { Question, Story } from \"../types\";\n\n/**\n * For each key in story.inputKeys, pick one question:\n * - If story.questionIds is set (same length as inputKeys), use those question ids from the bank.\n * - Otherwise pick one random question per answerKey.\n */\nexport function getQuestionsForStory(\n questionBank: Question[],\n story: Story\n): Question[] {\n const byId = new Map(questionBank.map((q) => [q.id, q]));\n\n if (story.questionIds && story.questionIds.length === story.inputKeys.length) {\n return story.questionIds.map((id) => {\n const q = byId.get(id);\n if (!q) throw new Error(`Question not found: ${id}`);\n return q;\n });\n }\n\n const byKey = new Map<string, Question[]>();\n for (const q of questionBank) {\n const list = byKey.get(q.answerKey) ?? [];\n list.push(q);\n byKey.set(q.answerKey, list);\n }\n\n return story.inputKeys.map((key) => {\n const list = byKey.get(key);\n if (!list || list.length === 0) {\n throw new Error(`No question found for answerKey: ${key}`);\n }\n return list[Math.floor(Math.random() * list.length)];\n });\n}\n","/** Нейтральные подстановки, чтобы пользователь никогда не видел «дырки» в тексте */\nconst DEFAULT_FALLBACKS: Record<string, string> = {\n name: \"the hero\",\n goal: \"a dream\",\n problem: \"doubts\",\n place: \"far away\",\n food: \"treasure\",\n thing: \"courage\",\n pet: \"a friend\",\n father: \"Dad\",\n mum: \"Mum\",\n neighbour: \"Mrs Brown\",\n friend: \"a friend\",\n profession: \"a hero\",\n enemy: \"the dragon\",\n hobby: \"dancing\",\n book: \"about adventures\",\n subject: \"nature\",\n librarian: \"the librarian\",\n genre: \"adventure\",\n style: \"ballet\",\n};\n\n/** Имена и обращения — всегда с большой буквы; профессия, еда, хобби — lower case */\nconst CAPITALIZE_KEYS = new Set([\n \"name\",\n \"father\",\n \"mum\",\n \"dad\",\n \"friend\",\n \"enemy\",\n \"groom\",\n \"pet\",\n \"coach\",\n \"teacher\",\n \"librarian\",\n \"neighbour\",\n]);\n\nfunction formatValue(key: string, raw: string): string {\n const s = raw.trim();\n if (s.length === 0) return s;\n if (CAPITALIZE_KEYS.has(key)) {\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n }\n return s.toLowerCase();\n}\n\n/**\n * Replaces placeholders {{key}} in text with values from answers.\n * name, father, friend, enemy → first letter uppercase; profession, food → lowercase.\n * Missing key → neutral fallback (never empty), so the sentence always reads well.\n */\nexport function renderText(text: string, answers: Record<string, string>): string {\n return text.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n const value = answers[key];\n if (value != null && value !== \"\") return formatValue(key, String(value));\n return DEFAULT_FALLBACKS[key] ?? \"something\";\n });\n}\n","import React, { useState, useCallback } from \"react\";\nimport type { Question, Story, Answers, Stage, GameEvent, StoryBuilderConfig } from \"../types\";\nimport { getQuestionsForStory } from \"../utils/getQuestionsForStory\";\nimport { renderText } from \"../utils/renderText\";\nimport cssStyles from \"./StoryBuilder.module.css\";\n\nconst INPUT_MAX_LENGTH = 40;\nconst LATIN_AND_SPACE_REGEX = /[^a-zA-Z ]/g;\n\nfunction sanitizeInput(value: string): string {\n return value.replace(LATIN_AND_SPACE_REGEX, \"\").slice(0, INPUT_MAX_LENGTH);\n}\n\nfunction capitalizeFirstLetter(value: string): string {\n const t = value.trim();\n if (!t) return value;\n return t.charAt(0).toUpperCase() + t.slice(1);\n}\n\nexport interface StoryBuilderGameProps {\n config?: StoryBuilderConfig;\n questions: Question[];\n stories: Story[];\n onEvent?: (event: GameEvent) => void;\n}\n\nexport function StoryBuilder({\n config,\n questions: questionBank,\n stories,\n onEvent,\n}: StoryBuilderGameProps) {\n const [stage, setStageState] = useState<Stage>(\"lobby\");\n const [currentStory, setCurrentStory] = useState<Story | null>(null);\n const [currentQuestions, setCurrentQuestions] = useState<Question[]>([]);\n const [answers, setAnswersState] = useState<Answers>({});\n const [slideIndex, setSlideIndexState] = useState(0);\n\n const goToStage = useCallback(\n (next: Stage) => {\n setStageState(next);\n onEvent?.({ type: \"STAGE_CHANGE\", stage: next });\n },\n [onEvent]\n );\n\n const setAnswer = useCallback(\n (answerKey: string, value: string) => {\n setAnswersState((prev) => ({ ...prev, [answerKey]: value }));\n const q = currentQuestions.find((c) => c.answerKey === answerKey);\n if (q) onEvent?.({ type: \"ANSWER_SUBMIT\", questionId: q.id, value });\n },\n [currentQuestions, onEvent]\n );\n\n const nextSlide = useCallback(() => {\n if (!currentStory) return;\n const contentLength = currentStory.slides.length;\n if (slideIndex < contentLength - 1) {\n const next = slideIndex + 1;\n setSlideIndexState(next);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: next });\n } else {\n if (currentStory.comprehensionQuestions?.length) {\n goToStage(\"comprehension\");\n } else {\n goToStage(\"end\");\n }\n }\n }, [currentStory, slideIndex, onEvent, goToStage]);\n\n const prevSlide = useCallback(() => {\n if (slideIndex > 0) {\n setSlideIndexState(slideIndex - 1);\n onEvent?.({ type: \"SLIDE_CHANGE\", index: slideIndex - 1 });\n }\n }, [slideIndex, onEvent]);\n\n const handleSelectStory = useCallback(\n (story: Story) => {\n const questionsForStory = getQuestionsForStory(questionBank, story);\n setCurrentStory(story);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n },\n [questionBank, goToStage]\n );\n\n const handleQuestionsSubmit = useCallback(() => {\n goToStage(\"story\");\n }, [goToStage]);\n\n const handleTryAgain = useCallback(() => {\n if (!currentStory) return;\n const questionsForStory = getQuestionsForStory(questionBank, currentStory);\n setCurrentQuestions(questionsForStory);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"questions\");\n }, [currentStory, questionBank, goToStage]);\n\n const handleReturnToLobby = useCallback(() => {\n setCurrentStory(null);\n setCurrentQuestions([]);\n setAnswersState({});\n setSlideIndexState(0);\n goToStage(\"lobby\");\n }, [goToStage]);\n\n if (stage === \"lobby\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.lobbyScreen}`}>\n <div className={cssStyles.container}>\n <div className={cssStyles.lobbyContent}>\n <h1 className={cssStyles.headline1}>Story Builder</h1>\n <p className={cssStyles.headline2}>Practice English with your own story</p>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => goToStage(\"storySelect\")}\n >\n Play\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"storySelect\") {\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Choose a story</h2>\n <div className={cssStyles.storyList}>\n {stories.map((story) => (\n <button\n key={story.id}\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={() => handleSelectStory(story)}\n >\n {story.title ?? story.id}\n </button>\n ))}\n </div>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.storySelectBack}`}\n onClick={() => goToStage(\"lobby\")}\n >\n Back\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"questions\") {\n const allFilled = currentQuestions.every(\n (q) => (answers[q.answerKey] ?? \"\").trim().length > 0\n );\n return (\n <div className={`${cssStyles.root} ${cssStyles.questionsScreen}`}>\n <div className={cssStyles.container}>\n <h2 className={cssStyles.headline2}>Answer a few questions</h2>\n <p className={cssStyles.inputHint}>Only Latin letters (A–Z) and spaces, max 40 characters per field.</p>\n {currentQuestions.map((q) => (\n <div key={q.id} className={cssStyles.inputGroup}>\n <label htmlFor={q.id}>{q.text}</label>\n <input\n id={q.id}\n type=\"text\"\n maxLength={INPUT_MAX_LENGTH}\n autoComplete=\"off\"\n placeholder={q.placeholder}\n value={answers[q.answerKey] ?? \"\"}\n onChange={(e) => {\n let v = sanitizeInput(e.target.value);\n if (q.answerKey === \"subject\") v = capitalizeFirstLetter(v);\n setAnswer(q.answerKey, v);\n }}\n />\n </div>\n ))}\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleQuestionsSubmit}\n disabled={!allFilled}\n >\n Continue\n </button>\n </div>\n </div>\n );\n }\n\n if (stage === \"story\" && currentStory) {\n const slide = currentStory.slides[slideIndex];\n let speechBubbleClass = cssStyles.slideSpeechBubble;\n\n if (\n slide?.speechBubblePosition === \"center\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s6\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleCenter}`;\n } else if (\n slide?.speechBubblePosition === \"bottom\" ||\n (slide?.speechBubblePosition === undefined &&\n slide?.id === \"s7\" &&\n currentStory.id === \"hero-1\")\n ) {\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleBottom}`;\n } else if (currentStory.id === \"hero-8\" && slide?.id === \"s6\") {\n // Финальный кадр School Concert: пузырь в верхнем левом углу, меньше по ширине\n speechBubbleClass = `${cssStyles.slideSpeechBubble} ${cssStyles.slideSpeechBubbleTopSmall}`;\n }\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.slideContainer}>\n <div className={cssStyles.slideFrame}>\n {/* Внутри кадра: фон + герой (единая сцена) */}\n {slide?.imageLayers?.length ? (\n <div className={cssStyles.slideLayers}>\n {slide.imageLayers.map((url, i) => {\n const isLast = i === slide.imageLayers!.length - 1;\n return (\n <img\n key={i}\n src={url}\n alt=\"\"\n className={isLast ? cssStyles.slideLayerCharacter : cssStyles.slideLayer}\n />\n );\n })}\n </div>\n ) : slide?.imageUrl ? (\n <img src={slide.imageUrl} alt=\"\" className={cssStyles.slideImage} />\n ) : (\n <div className={cssStyles.slideImage} aria-hidden />\n )}\n {/* Реплика: по slide.speechBubblePosition, с особыми случаями для hero-1 и финального кадра hero-8 */}\n <div className={speechBubbleClass} role=\"dialog\" aria-label=\"Hero says\">\n {slide ? renderText(slide.text, answers) : \"\"}\n </div>\n {/* Навигация внутри рамки, вторичная */}\n <div className={cssStyles.slideNav}>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={prevSlide}\n disabled={slideIndex === 0}\n aria-label=\"Previous slide\"\n >\n <img src=\"/slide-prev.png\" alt=\"\" />\n </button>\n <button\n type=\"button\"\n className={cssStyles.slideNavBtn}\n onClick={nextSlide}\n aria-label=\"Next slide\"\n >\n <img src=\"/slide-next.png\" alt=\"\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"comprehension\" && currentStory?.comprehensionQuestions?.length) {\n const compImageUrl = currentStory.comprehensionImageUrl;\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.comprehensionSlide}>\n {compImageUrl ? (\n <img src={compImageUrl} alt=\"\" className={cssStyles.comprehensionSlideImage} />\n ) : (\n <div className={cssStyles.comprehensionSlideImage} aria-hidden />\n )}\n {!currentStory.comprehensionQuestionsOnImage && (\n <div className={cssStyles.comprehensionBubble} role=\"dialog\" aria-label=\"Comprehension questions\">\n <ul className={cssStyles.comprehensionBubbleList}>\n {currentStory.comprehensionQuestions.map((q) => (\n <li key={q.id}>{q.text}</li>\n ))}\n </ul>\n </div>\n )}\n <div className={cssStyles.comprehensionActionsOnly}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n PLAY AGAIN\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n EXIT\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (stage === \"end\") {\n return (\n <div className={cssStyles.root}>\n <div className={cssStyles.container}>\n <h1 className={cssStyles.headline1}>The End</h1>\n <div className={cssStyles.endActions}>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleTryAgain}\n >\n Try again\n </button>\n <button\n type=\"button\"\n className={`${cssStyles.button} ${cssStyles.buttonLarge}`}\n onClick={handleReturnToLobby}\n >\n Return to lobby\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n return null;\n}\n"],"names":["getQuestionsForStory","questionBank","story","byId","q","id","byKey","list","key","DEFAULT_FALLBACKS","CAPITALIZE_KEYS","formatValue","raw","s","renderText","text","answers","_","value","INPUT_MAX_LENGTH","LATIN_AND_SPACE_REGEX","sanitizeInput","capitalizeFirstLetter","t","StoryBuilder","config","stories","onEvent","stage","setStageState","useState","currentStory","setCurrentStory","currentQuestions","setCurrentQuestions","setAnswersState","slideIndex","setSlideIndexState","goToStage","useCallback","next","setAnswer","answerKey","prev","c","nextSlide","contentLength","_a","prevSlide","handleSelectStory","questionsForStory","handleQuestionsSubmit","handleTryAgain","handleReturnToLobby","cssStyles","jsx","jsxs","allFilled","e","v","slide","speechBubbleClass","url","i","isLast","_b","compImageUrl"],"mappings":"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"}
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
._wrapperOuter_j1p5w_3{position:relative;width:100%;height:100%;overflow:hidden}._wrapperOuter_j1p5w_3 ._wrapperInner_j1p5w_10{position:absolute;left:50%;top:50%;box-sizing:border-box}._rotateScreen_j1p5w_18{width:100%;height:100vh;display:flex;justify-content:center;align-items:center;background-color:#fff7f5;flex-direction:column;text-align:center;padding:20px}._rotateScreenText_j1p5w_30{font-size:24px;font-weight:600;margin-bottom:12px;font-family:Onest,system-ui,sans-serif;color:#1f2937}._rotateScreenSubtext_j1p5w_38{font-size:16px;color:#6b7280;margin-bottom:24px}._rotateScreenIcon_j1p5w_44{font-size:48px;animation:_sbRotate_j1p5w_1 2s ease-in-out infinite}@keyframes _sbRotate_j1p5w_1{0%,to{transform:rotate(0)}50%{transform:rotate(90deg)}}._root_j1p5w_1{width:1280px;height:720px;position:relative;background-color:#fff7f5;overflow:hidden;font-family:Onest,system-ui,sans-serif;box-sizing:border-box}._root_j1p5w_1 *,._root_j1p5w_1 *:before,._root_j1p5w_1 *:after{box-sizing:border-box}._root_j1p5w_1 ._container_j1p5w_76{width:100%;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:24px;overflow:hidden}._root_j1p5w_1._lobbyScreen_j1p5w_89{background-image:url(/lobby-hero-cave-dragon.png);background-size:cover;background-position:center;background-repeat:no-repeat}._root_j1p5w_1._lobbyScreen_j1p5w_89 ._lobbyContent_j1p5w_96{background:#fff7f5e0;padding:32px 48px;border-radius:16px;box-shadow:0 4px 24px #00000014}._root_j1p5w_1 ._storyList_j1p5w_103{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 20px;align-items:center;justify-items:center;margin-top:16px;max-width:100%;max-height:420px;overflow-y:auto;padding:0 8px}._root_j1p5w_1 ._storyList_j1p5w_103 ._button_j1p5w_116{width:100%;max-width:320px;min-width:0}._root_j1p5w_1 ._storySelectBack_j1p5w_122{margin-top:24px}._root_j1p5w_1._questionsScreen_j1p5w_126{background-color:#fff7f5;background-image:url(/questions-bg.png);background-size:cover;background-position:center;background-repeat:no-repeat}._root_j1p5w_1 ._headline1_j1p5w_134{font-family:Onest,system-ui,sans-serif;font-size:32px;font-weight:600;color:#1f2937;margin-bottom:24px}._root_j1p5w_1 ._headline2_j1p5w_142{font-family:Onest,system-ui,sans-serif;font-size:24px;font-weight:500;color:#1f2937;margin-bottom:16px}._root_j1p5w_1 ._inputHint_j1p5w_150{font-size:14px;color:#6b7280;margin-bottom:16px}._root_j1p5w_1 ._headline3_j1p5w_156{font-family:Onest,system-ui,sans-serif;font-size:20px;font-weight:500;color:#1f2937;margin-bottom:12px}._root_j1p5w_1 ._button_j1p5w_116{font-family:Onest,system-ui,sans-serif;font-size:16px;padding:14px 36px;border-radius:10px;background:#f3f4f6;color:#1f2937;border:none;cursor:pointer;transition:all .2s ease}._root_j1p5w_1 ._button_j1p5w_116:hover{background:#ec4c44;color:#fff;transform:scale(1.05)}._root_j1p5w_1 ._button_j1p5w_116:active{transform:scale(.98)}._root_j1p5w_1 ._buttonLarge_j1p5w_186{padding:18px 24px;font-size:18px}._root_j1p5w_1 ._inputGroup_j1p5w_192{width:100%;max-width:560px;margin-bottom:16px;text-align:left}._root_j1p5w_1 ._inputGroup_j1p5w_192 label{display:block;font-size:14px;font-weight:500;color:#374151;margin-bottom:6px}._root_j1p5w_1 ._inputGroup_j1p5w_192 input{width:100%;font-family:Onest,system-ui,sans-serif;font-size:16px;padding:12px 16px;border:2px solid #e5e7eb;border-radius:10px;color:#1f2937;background:#fff}._root_j1p5w_1 ._inputGroup_j1p5w_192 input:focus{outline:none;border-color:#ec4c44}._root_j1p5w_1 ._slideContainer_j1p5w_224{width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:0}._root_j1p5w_1 ._slideFrame_j1p5w_234{position:relative;width:1280px;height:720px;overflow:hidden;border:2px solid #1f2937}._root_j1p5w_1 ._slideImage_j1p5w_242{position:absolute;top:0;left:0;width:100%;height:100%;background:#e5e7eb;object-fit:cover}._root_j1p5w_1 ._slideLayers_j1p5w_252{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;background:linear-gradient(to bottom,#87ceeb,#b0d8f0,#e8dcc4)}._root_j1p5w_1 ._slideLayer_j1p5w_252{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:cover;display:block}._root_j1p5w_1 ._slideLayerCharacter_j1p5w_272{position:absolute;left:50%;bottom:-2%;transform:translate(-50%);width:auto;max-width:32%;height:88%;object-fit:contain;object-position:bottom center;display:block;filter:drop-shadow(0 6px 12px rgba(0,0,0,.35))}._root_j1p5w_1 ._slideSpeechBubble_j1p5w_287{position:absolute;left:20px;top:20px;z-index:2;max-width:55%;padding:12px 18px;background:#fff;border:2px solid #1f2937;border-radius:14px;font-size:17px;line-height:1.45;color:#1f2937;text-align:left;box-shadow:0 2px 6px #0000001a}._root_j1p5w_1 ._slideSpeechBubbleCenter_j1p5w_305{left:50%;top:50%;bottom:auto;transform:translate(-50%,-50%);max-width:70%;max-height:80%;overflow-y:auto;text-align:center}._root_j1p5w_1 ._slideSpeechBubbleTopSmall_j1p5w_316{max-width:42%}._root_j1p5w_1 ._slideSpeechBubbleBottom_j1p5w_321{left:50%;top:auto;bottom:64px;transform:translate(-50%);max-width:85%;max-height:35%;overflow-y:auto;text-align:center}._root_j1p5w_1 ._slideNav_j1p5w_333{position:absolute;bottom:24px;left:0;right:0;height:52px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;pointer-events:none}._root_j1p5w_1 ._slideNav_j1p5w_333 button{pointer-events:auto}._root_j1p5w_1 ._slideNavBtn_j1p5w_350{padding:0;border:none;background:none;cursor:pointer;width:44px;height:44px;display:flex;align-items:center;justify-content:center;opacity:.85;transition:opacity .2s ease,transform .2s ease}._root_j1p5w_1 ._slideNavBtn_j1p5w_350:hover:not(:disabled){opacity:1;transform:scale(1.08)}._root_j1p5w_1 ._slideNavBtn_j1p5w_350:active:not(:disabled){transform:scale(.95)}._root_j1p5w_1 ._slideNavBtn_j1p5w_350:disabled{opacity:.35;cursor:not-allowed}._root_j1p5w_1 ._slideNavBtn_j1p5w_350 img{width:100%;height:100%;object-fit:contain;display:block}._root_j1p5w_1 ._comprehensionSlide_j1p5w_386{position:relative;width:100%;height:100%;overflow:hidden}._root_j1p5w_1 ._comprehensionSlideImage_j1p5w_393{position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(to bottom,#87ceeb,#b0d8f0,#e8dcc4);object-fit:cover}._root_j1p5w_1 ._comprehensionOverlay_j1p5w_403{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px 100px;background:#fff7f5d1;text-align:center}._root_j1p5w_1 ._comprehensionBubble_j1p5w_416{position:absolute;left:50%;top:48px;transform:translate(-50%);z-index:2;max-width:min(520px,calc(100% - 48px));padding:24px 32px 28px;background:linear-gradient(165deg,#fffef9,#fdf8f0);border-radius:20px;box-shadow:0 4px 20px #00000014,0 0 0 1px #0000000f,inset 0 1px #ffffffe6;border:none}._root_j1p5w_1 ._comprehensionBubble_j1p5w_416:after{content:"";position:absolute;bottom:-12px;left:50%;transform:translate(-50%);border:14px solid transparent;border-top-color:#fdf8f0;filter:drop-shadow(0 2px 4px rgba(0,0,0,.06))}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444{list-style:none;padding:0;margin:0;text-align:center}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444 li{font-size:18px;font-weight:500;line-height:1.5;color:#2d3748;margin-bottom:12px;padding:0 8px}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444 li:last-child{margin-bottom:0}._root_j1p5w_1 ._comprehensionBubbleList_j1p5w_444 li:before{content:none}._root_j1p5w_1 ._comprehensionActionsOnly_j1p5w_469{position:absolute;bottom:32px;left:50%;transform:translate(-50%);display:flex;gap:24px;z-index:2}._root_j1p5w_1 ._comprehensionTitle_j1p5w_479{font-family:Onest,system-ui,sans-serif;font-size:22px;font-weight:600;color:#1f2937;margin-bottom:24px}._root_j1p5w_1 ._comprehensionQuestions_j1p5w_487{list-style:none;padding:0;margin:0 0 32px;max-width:560px;text-align:left}._root_j1p5w_1 ._comprehensionQuestions_j1p5w_487 li{font-size:18px;line-height:1.5;color:#374151;margin-bottom:14px;padding-left:1.2em;position:relative}._root_j1p5w_1 ._comprehensionQuestions_j1p5w_487 li:before{content:"•";position:absolute;left:0;color:#ec4c44;font-weight:700}._root_j1p5w_1 ._comprehensionActions_j1p5w_469{display:flex;gap:24px;margin-top:auto}._root_j1p5w_1 ._comprehensionList_j1p5w_519{width:100%;max-width:560px;text-align:left;margin-bottom:24px}._root_j1p5w_1 ._comprehensionList_j1p5w_519 ._inputGroup_j1p5w_192{margin-bottom:20px}._root_j1p5w_1 ._endActions_j1p5w_531{display:flex;gap:24px;margin-top:24px}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Question from the bank: answerKey is used for substitution in stories */
|
|
2
|
+
export interface Question {
|
|
3
|
+
id: string;
|
|
4
|
+
answerKey: string;
|
|
5
|
+
text: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Position of the speech bubble on the slide */
|
|
9
|
+
export type SpeechBubblePosition = "default" | "center" | "bottom";
|
|
10
|
+
export interface Slide {
|
|
11
|
+
id: string;
|
|
12
|
+
text: string;
|
|
13
|
+
imageUrl: string;
|
|
14
|
+
/** When set, slide image is composed from these layers (bottom to top). Takes precedence over imageUrl. */
|
|
15
|
+
imageLayers?: string[];
|
|
16
|
+
/** When set, overrides default (top-left) placement of the speech bubble. */
|
|
17
|
+
speechBubblePosition?: SpeechBubblePosition;
|
|
18
|
+
onEnter?: unknown[];
|
|
19
|
+
}
|
|
20
|
+
export interface ComprehensionQuestion {
|
|
21
|
+
id: string;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
/** Story is the main object: it declares inputKeys and comprehension */
|
|
25
|
+
export interface Story {
|
|
26
|
+
id: string;
|
|
27
|
+
/** Display name in lobby (optional; falls back to id) */
|
|
28
|
+
title?: string;
|
|
29
|
+
slides: Slide[];
|
|
30
|
+
inputKeys: string[];
|
|
31
|
+
/** If set, use these question ids in order (one per inputKey); otherwise pick at random by answerKey */
|
|
32
|
+
questionIds?: string[];
|
|
33
|
+
comprehensionQuestions?: ComprehensionQuestion[];
|
|
34
|
+
/** Optional background image for the comprehension screen (e.g. village with hero, no crowd) */
|
|
35
|
+
comprehensionImageUrl?: string;
|
|
36
|
+
/** When true, questions are drawn on the comprehension image; do not show the overlay bubble */
|
|
37
|
+
comprehensionQuestionsOnImage?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Answers collected from user: answerKey → value */
|
|
40
|
+
export type Answers = Record<string, string>;
|
|
41
|
+
export type Stage = "lobby" | "storySelect" | "questions" | "story" | "comprehension" | "end";
|
|
42
|
+
export type GameEvent = {
|
|
43
|
+
type: "STAGE_CHANGE";
|
|
44
|
+
stage: Stage;
|
|
45
|
+
} | {
|
|
46
|
+
type: "ANSWER_SUBMIT";
|
|
47
|
+
questionId: string;
|
|
48
|
+
value: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: "SLIDE_CHANGE";
|
|
51
|
+
index: number;
|
|
52
|
+
};
|
|
53
|
+
export interface StoryBuilderConfig {
|
|
54
|
+
/** Number of questions per round (default 5); first key is always name */
|
|
55
|
+
questionsPerRound?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface StoryBuilderProps {
|
|
58
|
+
config?: StoryBuilderConfig;
|
|
59
|
+
questions: Question[];
|
|
60
|
+
stories: Story[];
|
|
61
|
+
onEvent?: (event: GameEvent) => void;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,2GAA2G;IAC3G,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,6EAA6E;IAC7E,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wEAAwE;AACxE,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,wGAAwG;IACxG,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sBAAsB,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACjD,gGAAgG;IAChG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gGAAgG;IAChG,6BAA6B,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,qDAAqD;AACrD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE7C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,eAAe,GAAG,KAAK,CAAC;AAE9F,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Question, Story } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* For each key in story.inputKeys, pick one question:
|
|
4
|
+
* - If story.questionIds is set (same length as inputKeys), use those question ids from the bank.
|
|
5
|
+
* - Otherwise pick one random question per answerKey.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getQuestionsForStory(questionBank: Question[], story: Story): Question[];
|
|
8
|
+
//# sourceMappingURL=getQuestionsForStory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getQuestionsForStory.d.ts","sourceRoot":"","sources":["../../src/utils/getQuestionsForStory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,QAAQ,EAAE,EACxB,KAAK,EAAE,KAAK,GACX,QAAQ,EAAE,CAyBZ"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces placeholders {{key}} in text with values from answers.
|
|
3
|
+
* name, father, friend, enemy → first letter uppercase; profession, food → lowercase.
|
|
4
|
+
* Missing key → neutral fallback (never empty), so the sentence always reads well.
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderText(text: string, answers: Record<string, string>): string;
|
|
7
|
+
//# sourceMappingURL=renderText.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderText.d.ts","sourceRoot":"","sources":["../../src/utils/renderText.ts"],"names":[],"mappings":"AAgDA;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAMhF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "speakid-story-builder",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Story Builder - Interactive story game for English practice (SPEAKID)",
|
|
5
|
+
"main": "dist/speakid-story-builder.cjs.js",
|
|
6
|
+
"module": "dist/speakid-story-builder.es.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/speakid-story-builder.es.js",
|
|
13
|
+
"require": "./dist/speakid-story-builder.cjs.js",
|
|
14
|
+
"default": "./dist/speakid-story-builder.es.js"
|
|
15
|
+
},
|
|
16
|
+
"./style.css": "./dist/style.css"
|
|
17
|
+
},
|
|
18
|
+
"style": "./dist/style.css",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "vite",
|
|
25
|
+
"build": "vite build && npm run build:types",
|
|
26
|
+
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"preview": "vite preview"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"speakid",
|
|
32
|
+
"game",
|
|
33
|
+
"education",
|
|
34
|
+
"story-builder",
|
|
35
|
+
"english",
|
|
36
|
+
"react",
|
|
37
|
+
"typescript"
|
|
38
|
+
],
|
|
39
|
+
"author": "SPEAKID Team",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18.2.0",
|
|
43
|
+
"react-dom": ">=18.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^24.9.1",
|
|
47
|
+
"@types/react": "^18.2.0",
|
|
48
|
+
"@types/react-dom": "^18.2.0",
|
|
49
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
50
|
+
"react": "^19.2.0",
|
|
51
|
+
"react-dom": "^19.2.0",
|
|
52
|
+
"typescript": "^5.2.0",
|
|
53
|
+
"vite": "^5.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|