@idosgames/mcp 0.1.47 → 0.1.48

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.
Files changed (37) hide show
  1. package/dist/cli.js +1 -1
  2. package/package.json +1 -1
  3. package/registry/host.json +1 -1
  4. package/registry/index.json +36 -36
  5. package/registry/modules/ai-media-app.json +5 -5
  6. package/registry/modules/ai.json +1 -1
  7. package/registry/modules/board-game.json +1 -1
  8. package/registry/modules/boosts.json +1 -1
  9. package/registry/modules/character.json +1 -1
  10. package/registry/modules/chat.json +1 -1
  11. package/registry/modules/collections.json +1 -1
  12. package/registry/modules/coop-events.json +1 -1
  13. package/registry/modules/craft.json +1 -1
  14. package/registry/modules/deals.json +1 -1
  15. package/registry/modules/events.json +1 -1
  16. package/registry/modules/friends.json +1 -1
  17. package/registry/modules/hero-run.json +1 -1
  18. package/registry/modules/idle-rpg.json +1 -1
  19. package/registry/modules/inventory.json +1 -1
  20. package/registry/modules/invite.json +1 -1
  21. package/registry/modules/kit-game.json +1 -1
  22. package/registry/modules/leaderboard.json +1 -1
  23. package/registry/modules/lootboxes.json +1 -1
  24. package/registry/modules/mailbox.json +1 -1
  25. package/registry/modules/marketplace.json +1 -1
  26. package/registry/modules/multiplayer.json +1 -1
  27. package/registry/modules/premium.json +1 -1
  28. package/registry/modules/profile.json +1 -1
  29. package/registry/modules/pvp.json +1 -1
  30. package/registry/modules/quests.json +1 -1
  31. package/registry/modules/rewards.json +1 -1
  32. package/registry/modules/season.json +1 -1
  33. package/registry/modules/store.json +1 -1
  34. package/registry/modules/tutorials.json +1 -1
  35. package/registry/modules/video-studio.json +1 -1
  36. package/registry/modules/voxelcraft.json +1 -1
  37. package/registry/modules/workshop.json +1 -1
package/dist/cli.js CHANGED
@@ -2303,7 +2303,7 @@ async function callVideoTool(name, args) {
2303
2303
  }
2304
2304
 
2305
2305
  // src/server.ts
2306
- var SERVER_VERSION = true ? "0.1.47" : "0.0.0-dev";
2306
+ var SERVER_VERSION = true ? "0.1.48" : "0.0.0-dev";
2307
2307
  var readOnly2 = (title) => ({
2308
2308
  title,
2309
2309
  readOnlyHint: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idosgames/mcp",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "mcpName": "com.idosgames/sdk",
5
5
  "description": "MCP server that serves the iDosGames Module & Skills Registry to AI coding agents (Claude Code, Codex, Cursor…): list/pull composable game modules and the host scaffold, and load skills for @idosgames/core, the module contract, and composition.",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  {
25
25
  "path": "package.json",
26
- "content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (both from @idosgames/app-shell) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.16.7\",\n \"@idosgames/core\": \"0.26.2\",\n \"@idosgames/module-sdk\": \"0.13.4\",\n \"@idosgames/react\": \"0.7.2\",\n \"@idosgames/wallet\": \"0.6.10\",\n \"@reown/appkit\": \"1.8.23\",\n \"@reown/appkit-adapter-wagmi\": \"1.8.23\",\n \"@tanstack/react-query\": \"5.101.2\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\",\n \"wagmi\": \"3.7.2\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"6.0.4\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"8.1.5\"\n }\n}\n"
26
+ "content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (both from @idosgames/app-shell) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.16.7\",\n \"@idosgames/core\": \"0.26.3\",\n \"@idosgames/module-sdk\": \"0.13.4\",\n \"@idosgames/react\": \"0.7.2\",\n \"@idosgames/wallet\": \"0.6.10\",\n \"@reown/appkit\": \"1.8.23\",\n \"@reown/appkit-adapter-wagmi\": \"1.8.23\",\n \"@tanstack/react-query\": \"5.101.2\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\",\n \"wagmi\": \"3.7.2\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"6.0.4\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"8.1.5\"\n }\n}\n"
27
27
  },
28
28
  {
29
29
  "path": "public/sw.js",
@@ -1,7 +1,7 @@
1
1
  {
2
- "generatedFromCommit": "a2bb75b01b40a3c3d5740e67ff234afae5024d93",
2
+ "generatedFromCommit": "f37b01d48bc0a488710aa159b05b51ccffbcb1a7",
3
3
  "runtimePackages": {
4
- "@idosgames/core": "0.26.2",
4
+ "@idosgames/core": "0.26.3",
5
5
  "@idosgames/wallet": "0.6.10",
6
6
  "@idosgames/module-sdk": "0.13.4",
7
7
  "@idosgames/react": "0.7.2",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "version": "0.1.4",
50
50
  "dependencies": {
51
- "@idosgames/core": "0.26.2",
51
+ "@idosgames/core": "0.26.3",
52
52
  "@idosgames/module-sdk": "0.13.4",
53
53
  "@idosgames/react": "0.7.2",
54
54
  "react": "19.2.7"
@@ -85,7 +85,7 @@
85
85
  },
86
86
  "version": "0.1.3",
87
87
  "dependencies": {
88
- "@idosgames/core": "0.26.2",
88
+ "@idosgames/core": "0.26.3",
89
89
  "@idosgames/module-sdk": "0.13.4",
90
90
  "@idosgames/react": "0.7.2",
91
91
  "react": "19.2.7"
@@ -144,7 +144,7 @@
144
144
  ]
145
145
  },
146
146
  "dependencies": {
147
- "@idosgames/core": "0.26.2",
147
+ "@idosgames/core": "0.26.3",
148
148
  "@idosgames/module-sdk": "0.13.4",
149
149
  "@idosgames/react": "0.7.2",
150
150
  "react": "19.2.7"
@@ -181,7 +181,7 @@
181
181
  },
182
182
  "version": "0.1.1",
183
183
  "dependencies": {
184
- "@idosgames/core": "0.26.2",
184
+ "@idosgames/core": "0.26.3",
185
185
  "@idosgames/module-sdk": "0.13.4",
186
186
  "@idosgames/react": "0.7.2",
187
187
  "react": "19.2.7"
@@ -218,7 +218,7 @@
218
218
  },
219
219
  "version": "0.1.4",
220
220
  "dependencies": {
221
- "@idosgames/core": "0.26.2",
221
+ "@idosgames/core": "0.26.3",
222
222
  "@idosgames/module-sdk": "0.13.4",
223
223
  "@idosgames/react": "0.7.2",
224
224
  "react": "19.2.7"
@@ -254,7 +254,7 @@
254
254
  },
255
255
  "version": "0.1.1",
256
256
  "dependencies": {
257
- "@idosgames/core": "0.26.2",
257
+ "@idosgames/core": "0.26.3",
258
258
  "@idosgames/module-sdk": "0.13.4",
259
259
  "@idosgames/react": "0.7.2",
260
260
  "react": "19.2.7"
@@ -295,7 +295,7 @@
295
295
  },
296
296
  "version": "0.1.5",
297
297
  "dependencies": {
298
- "@idosgames/core": "0.26.2",
298
+ "@idosgames/core": "0.26.3",
299
299
  "@idosgames/module-sdk": "0.13.4",
300
300
  "@idosgames/react": "0.7.2",
301
301
  "react": "19.2.7"
@@ -331,7 +331,7 @@
331
331
  },
332
332
  "version": "0.1.3",
333
333
  "dependencies": {
334
- "@idosgames/core": "0.26.2",
334
+ "@idosgames/core": "0.26.3",
335
335
  "@idosgames/module-sdk": "0.13.4",
336
336
  "@idosgames/react": "0.7.2",
337
337
  "react": "19.2.7"
@@ -370,7 +370,7 @@
370
370
  },
371
371
  "version": "0.1.3",
372
372
  "dependencies": {
373
- "@idosgames/core": "0.26.2",
373
+ "@idosgames/core": "0.26.3",
374
374
  "@idosgames/module-sdk": "0.13.4",
375
375
  "@idosgames/react": "0.7.2",
376
376
  "react": "19.2.7"
@@ -409,7 +409,7 @@
409
409
  },
410
410
  "version": "0.1.3",
411
411
  "dependencies": {
412
- "@idosgames/core": "0.26.2",
412
+ "@idosgames/core": "0.26.3",
413
413
  "@idosgames/module-sdk": "0.13.4",
414
414
  "@idosgames/react": "0.7.2",
415
415
  "react": "19.2.7"
@@ -449,7 +449,7 @@
449
449
  },
450
450
  "version": "0.1.2",
451
451
  "dependencies": {
452
- "@idosgames/core": "0.26.2",
452
+ "@idosgames/core": "0.26.3",
453
453
  "@idosgames/module-sdk": "0.13.4",
454
454
  "@idosgames/react": "0.7.2",
455
455
  "react": "19.2.7"
@@ -490,7 +490,7 @@
490
490
  },
491
491
  "version": "0.1.3",
492
492
  "dependencies": {
493
- "@idosgames/core": "0.26.2",
493
+ "@idosgames/core": "0.26.3",
494
494
  "@idosgames/module-sdk": "0.13.4",
495
495
  "@idosgames/react": "0.7.2",
496
496
  "react": "19.2.7"
@@ -529,7 +529,7 @@
529
529
  },
530
530
  "version": "0.1.3",
531
531
  "dependencies": {
532
- "@idosgames/core": "0.26.2",
532
+ "@idosgames/core": "0.26.3",
533
533
  "@idosgames/module-sdk": "0.13.4",
534
534
  "@idosgames/react": "0.7.2",
535
535
  "react": "19.2.7"
@@ -571,7 +571,7 @@
571
571
  },
572
572
  "version": "0.1.5",
573
573
  "dependencies": {
574
- "@idosgames/core": "0.26.2",
574
+ "@idosgames/core": "0.26.3",
575
575
  "@idosgames/module-sdk": "0.13.4",
576
576
  "@idosgames/react": "0.7.2",
577
577
  "react": "19.2.7"
@@ -608,7 +608,7 @@
608
608
  },
609
609
  "version": "0.1.3",
610
610
  "dependencies": {
611
- "@idosgames/core": "0.26.2",
611
+ "@idosgames/core": "0.26.3",
612
612
  "@idosgames/module-sdk": "0.13.4",
613
613
  "@idosgames/react": "0.7.2",
614
614
  "react": "19.2.7"
@@ -649,7 +649,7 @@
649
649
  },
650
650
  "version": "0.1.3",
651
651
  "dependencies": {
652
- "@idosgames/core": "0.26.2",
652
+ "@idosgames/core": "0.26.3",
653
653
  "@idosgames/module-sdk": "0.13.4",
654
654
  "@idosgames/react": "0.7.2",
655
655
  "react": "19.2.7"
@@ -689,7 +689,7 @@
689
689
  },
690
690
  "version": "0.1.3",
691
691
  "dependencies": {
692
- "@idosgames/core": "0.26.2",
692
+ "@idosgames/core": "0.26.3",
693
693
  "@idosgames/module-sdk": "0.13.4",
694
694
  "@idosgames/react": "0.7.2",
695
695
  "react": "19.2.7"
@@ -729,7 +729,7 @@
729
729
  },
730
730
  "version": "0.1.2",
731
731
  "dependencies": {
732
- "@idosgames/core": "0.26.2",
732
+ "@idosgames/core": "0.26.3",
733
733
  "@idosgames/module-sdk": "0.13.4",
734
734
  "@idosgames/react": "0.7.2",
735
735
  "react": "19.2.7"
@@ -769,7 +769,7 @@
769
769
  },
770
770
  "version": "0.1.3",
771
771
  "dependencies": {
772
- "@idosgames/core": "0.26.2",
772
+ "@idosgames/core": "0.26.3",
773
773
  "@idosgames/module-sdk": "0.13.4",
774
774
  "@idosgames/react": "0.7.2",
775
775
  "react": "19.2.7"
@@ -810,7 +810,7 @@
810
810
  },
811
811
  "version": "0.1.3",
812
812
  "dependencies": {
813
- "@idosgames/core": "0.26.2",
813
+ "@idosgames/core": "0.26.3",
814
814
  "@idosgames/module-sdk": "0.13.4",
815
815
  "@idosgames/react": "0.7.2",
816
816
  "react": "19.2.7"
@@ -848,7 +848,7 @@
848
848
  },
849
849
  "version": "0.1.2",
850
850
  "dependencies": {
851
- "@idosgames/core": "0.26.2",
851
+ "@idosgames/core": "0.26.3",
852
852
  "@idosgames/module-sdk": "0.13.4",
853
853
  "@idosgames/react": "0.7.2",
854
854
  "react": "19.2.7"
@@ -892,7 +892,7 @@
892
892
  },
893
893
  "version": "0.2.1",
894
894
  "dependencies": {
895
- "@idosgames/core": "0.26.2",
895
+ "@idosgames/core": "0.26.3",
896
896
  "@idosgames/module-sdk": "0.13.4",
897
897
  "@idosgames/react": "0.7.2",
898
898
  "react": "19.2.7"
@@ -931,7 +931,7 @@
931
931
  },
932
932
  "version": "0.1.5",
933
933
  "dependencies": {
934
- "@idosgames/core": "0.26.2",
934
+ "@idosgames/core": "0.26.3",
935
935
  "@idosgames/module-sdk": "0.13.4",
936
936
  "@idosgames/react": "0.7.2",
937
937
  "react": "19.2.7"
@@ -968,7 +968,7 @@
968
968
  },
969
969
  "version": "0.1.4",
970
970
  "dependencies": {
971
- "@idosgames/core": "0.26.2",
971
+ "@idosgames/core": "0.26.3",
972
972
  "@idosgames/module-sdk": "0.13.4",
973
973
  "@idosgames/react": "0.7.2",
974
974
  "react": "19.2.7"
@@ -1012,7 +1012,7 @@
1012
1012
  },
1013
1013
  "version": "0.1.3",
1014
1014
  "dependencies": {
1015
- "@idosgames/core": "0.26.2",
1015
+ "@idosgames/core": "0.26.3",
1016
1016
  "@idosgames/module-sdk": "0.13.4",
1017
1017
  "@idosgames/react": "0.7.2",
1018
1018
  "react": "19.2.7"
@@ -1047,7 +1047,7 @@
1047
1047
  },
1048
1048
  "version": "0.1.3",
1049
1049
  "dependencies": {
1050
- "@idosgames/core": "0.26.2",
1050
+ "@idosgames/core": "0.26.3",
1051
1051
  "@idosgames/module-sdk": "0.13.4",
1052
1052
  "@idosgames/react": "0.7.2",
1053
1053
  "react": "19.2.7"
@@ -1097,7 +1097,7 @@
1097
1097
  ]
1098
1098
  },
1099
1099
  "dependencies": {
1100
- "@idosgames/core": "0.26.2",
1100
+ "@idosgames/core": "0.26.3",
1101
1101
  "@idosgames/module-sdk": "0.13.4",
1102
1102
  "@idosgames/react": "0.7.2",
1103
1103
  "@idosgames/wallet": "0.6.10",
@@ -1183,7 +1183,7 @@
1183
1183
  },
1184
1184
  "dependencies": {
1185
1185
  "@idosgames/characters": "0.1.3",
1186
- "@idosgames/core": "0.26.2",
1186
+ "@idosgames/core": "0.26.3",
1187
1187
  "@idosgames/module-sdk": "0.13.4",
1188
1188
  "@idosgames/react": "0.7.2",
1189
1189
  "phaser": "4.2.1",
@@ -1259,7 +1259,7 @@
1259
1259
  },
1260
1260
  "dependencies": {
1261
1261
  "@idosgames/characters": "0.1.3",
1262
- "@idosgames/core": "0.26.2",
1262
+ "@idosgames/core": "0.26.3",
1263
1263
  "@idosgames/module-sdk": "0.13.4",
1264
1264
  "@idosgames/react": "0.7.2",
1265
1265
  "phaser": "4.2.1",
@@ -1350,7 +1350,7 @@
1350
1350
  "version": "0.7.4",
1351
1351
  "dependencies": {
1352
1352
  "@idosgames/characters": "0.1.3",
1353
- "@idosgames/core": "0.26.2",
1353
+ "@idosgames/core": "0.26.3",
1354
1354
  "@idosgames/module-sdk": "0.13.4",
1355
1355
  "@idosgames/react": "0.7.2",
1356
1356
  "react": "19.2.7",
@@ -1396,7 +1396,7 @@
1396
1396
  "version": "0.1.0",
1397
1397
  "dependencies": {
1398
1398
  "@idosgames/characters": "0.1.3",
1399
- "@idosgames/core": "0.26.2",
1399
+ "@idosgames/core": "0.26.3",
1400
1400
  "@idosgames/game-kit": "0.1.1",
1401
1401
  "@idosgames/module-sdk": "0.13.4",
1402
1402
  "@idosgames/react": "0.7.2",
@@ -1458,7 +1458,7 @@
1458
1458
  "version": "0.1.1",
1459
1459
  "dependencies": {
1460
1460
  "@idosgames/characters": "0.1.3",
1461
- "@idosgames/core": "0.26.2",
1461
+ "@idosgames/core": "0.26.3",
1462
1462
  "@idosgames/module-sdk": "0.13.4",
1463
1463
  "@idosgames/react": "0.7.2",
1464
1464
  "@idosgames/timeline": "0.1.0",
@@ -1503,13 +1503,13 @@
1503
1503
  },
1504
1504
  "version": "0.1.0",
1505
1505
  "dependencies": {
1506
- "@idosgames/core": "0.26.2",
1506
+ "@idosgames/core": "0.26.3",
1507
1507
  "@idosgames/module-sdk": "0.13.4",
1508
1508
  "@idosgames/react": "0.7.2",
1509
1509
  "react": "19.2.7"
1510
1510
  },
1511
1511
  "fileCount": 7,
1512
- "contentHash": "cbe4819d76f43427c02c27358065e1afba58996ec755b416559940fe6ee47093"
1512
+ "contentHash": "8f0a30d2553123db9257c8d6989281f45b7738468ad01b95fbe92e12c735cf2f"
1513
1513
  }
1514
1514
  ],
1515
1515
  "skills": [
@@ -38,20 +38,20 @@
38
38
  "version": "0.1.0"
39
39
  },
40
40
  "dependencies": {
41
- "@idosgames/core": "0.26.2",
41
+ "@idosgames/core": "0.26.3",
42
42
  "@idosgames/module-sdk": "0.13.4",
43
43
  "@idosgames/react": "0.7.2",
44
44
  "react": "19.2.7"
45
45
  },
46
- "contentHash": "cbe4819d76f43427c02c27358065e1afba58996ec755b416559940fe6ee47093",
46
+ "contentHash": "8f0a30d2553123db9257c8d6989281f45b7738468ad01b95fbe92e12c735cf2f",
47
47
  "files": [
48
48
  {
49
49
  "path": "App.tsx",
50
- "content": "import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport type {\n AIGenerationView,\n AIInputImage,\n AIPublicDefinitions,\n AIPublicTemplate,\n IDosGamesClient,\n} from \"@idosgames/core\";\nimport { canPayWithPlatformCredits } from \"@idosgames/core\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n canAfford,\n errorText,\n formatLineAmount,\n Icon,\n isCreditsCancel,\n payWithCredits,\n pickLanguage,\n PlatformCurrencyIcon,\n useCatalog,\n useToast,\n type ResourceLine,\n} from \"@idosgames/react/ui\";\nimport {\n appItems,\n fitSize,\n kindsOf,\n localized,\n missingInputs,\n missingValues,\n priceLabelOf,\n resultKind,\n type AppItem,\n type Kind,\n} from \"./model\";\nimport { t } from \"./i18n\";\nimport { APP_CSS, FONT_URL } from \"./styles\";\n\n// The AI generator app: the title's AI features as cards (trends and templates of the platform AI studio, and\n// \"describe it yourself\" for a kind with a plain feature). Prices, free generations and the cards come from the\n// engine's sanitized AI definitions; the template's prompt never reaches the player.\n//\n// The look is idosgames.com, not a game (owner 09.11.2026): dark zinc, the site's blue, Onest, flat cards; and as few\n// buttons as possible: a card → a photo → ONE button with the price → the result with Download and Again.\n\ntype Screen = { at: \"home\" } | { at: \"run\"; key: string } | { at: \"works\" };\n\n/**\n * The AI definitions of the title: read once, read again after a generation (the free count moves). Always from the\n * engine (`forceRefresh`), never the cached config section: only the engine's answer carries the template cards\n * (`PlatformTemplate`) and the player's `FreeLeftToday`; the config copy has neither, and every template card would\n * be dropped as \"taken off the site\".\n */\nfunction useDefinitions(client: IDosGamesClient): {\n defs: AIPublicDefinitions | null;\n refresh: () => void;\n} {\n const [defs, setDefs] = useState<AIPublicDefinitions | null>(null);\n const refresh = useCallback(() => {\n void client.ai\n .getDefinitions({ forceRefresh: true })\n .then((r) => setDefs(r.ok ? r.data : { Enabled: false }));\n }, [client]);\n useEffect(refresh, [refresh]);\n return { defs, refresh };\n}\n\n/** The site's font, once per page. */\nfunction useSiteFont(): void {\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n if (document.querySelector(`link[href=\"${FONT_URL}\"]`)) return;\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = FONT_URL;\n document.head.appendChild(link);\n }, []);\n}\n\nexport function MediaApp(): ReactNode {\n useSiteFont();\n const client = useIDosGamesClient();\n const { defs, refresh } = useDefinitions(client);\n // In the iOS app Aspis cannot be paid (App Store 3.1.1): its prices are dropped, those cards only show the preview.\n const items = appItems(defs, canPayWithPlatformCredits());\n const kinds = kindsOf(items);\n const [kind, setKind] = useState<Kind | null>(null);\n const [screen, setScreen] = useState<Screen>({ at: \"home\" });\n const home = () => setScreen({ at: \"home\" });\n\n const current = kind && kinds.includes(kind) ? kind : kinds[0];\n const item =\n screen.at === \"run\" ? items.find((i) => i.key === screen.key) : undefined;\n\n return (\n <div className=\"iam\">\n <style>{APP_CSS}</style>\n {!defs ? (\n <div className=\"iam-center\">\n <span className=\"iam-spin\" />\n </div>\n ) : item ? (\n <Run key={item.key} item={item} onBack={home} onDone={refresh} />\n ) : screen.at === \"works\" ? (\n <Works items={items} onBack={home} />\n ) : (\n <>\n <header className=\"iam-bar\">\n <span className=\"iam-title\">{appTitle()}</span>\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"history\")}\n onClick={() => setScreen({ at: \"works\" })}\n >\n <Svg d={ICON.works} />\n </button>\n </header>\n {kinds.length > 1 ? (\n <nav className=\"iam-chips\">\n {kinds.map((k) => (\n <button\n key={k}\n type=\"button\"\n className={\n k === current ? \"iam-chip iam-chip-on\" : \"iam-chip\"\n }\n onClick={() => setKind(k)}\n >\n {t(`k_${k}`)}\n </button>\n ))}\n </nav>\n ) : null}\n {current ? (\n <Grid\n items={items.filter((i) => i.kind === current)}\n onOpen={(key) => setScreen({ at: \"run\", key })}\n />\n ) : (\n <p className=\"iam-empty\">{t(\"empty\")}</p>\n )}\n </>\n )}\n </div>\n );\n}\n\n/** The app's name: the page title the author set, else \"AI generator\". */\nfunction appTitle(): string {\n const title = typeof document === \"undefined\" ? \"\" : document.title.trim();\n return title || t(\"title\");\n}\n\n// ── Home: one grid of big cards, trends first ────────────────────────────────────────────────\n\nfunction Grid({\n items,\n onOpen,\n}: {\n items: AppItem[];\n onOpen: (key: string) => void;\n}): ReactNode {\n // Trends first, then templates, \"describe it yourself\" last: a ready card converts better than an empty box.\n const ordered = [\n ...items.filter((i) => i.trend),\n ...items.filter((i) => i.template && !i.trend),\n ...items.filter((i) => !i.template),\n ];\n return (\n <div className=\"iam-grid\">\n {ordered.map((i) => (\n <button\n key={i.key}\n type=\"button\"\n className={i.kind === \"Video\" ? \"iam-card iam-tall\" : \"iam-card\"}\n onClick={() => onOpen(i.key)}\n >\n {i.template ? (\n <Preview template={i.template} cover />\n ) : (\n <span className=\"iam-own\">\n <Svg d={ICON.pen} size={28} />\n </span>\n )}\n {i.trend ? <span className=\"iam-badge\">{t(\"trend\")}</span> : null}\n <span className=\"iam-card-foot\">\n <span className=\"iam-card-name\">\n {i.template ? localized(i.template.Name, lang()) : t(\"own\")}\n </span>\n <PriceChip item={i} />\n </span>\n </button>\n ))}\n </div>\n );\n}\n\nconst lang = (): string => pickLanguage();\n\n/** The sample of a template: a silent looping video or a picture; the robot when there is none. */\nfunction Preview({\n template,\n cover,\n}: {\n template: AIPublicTemplate;\n cover?: boolean;\n}): ReactNode {\n const p = template.Preview;\n const cls = cover ? \"iam-media iam-cover\" : \"iam-media\";\n const type = (p?.Type ?? \"\").toLowerCase();\n if (!p?.Url || type === \"audio\")\n return (\n <span className=\"iam-own\">\n <Svg d={ICON.bot} size={32} />\n </span>\n );\n if (type === \"video\")\n return (\n <video\n className={cls}\n src={p.Url}\n poster={p.PosterUrl ?? undefined}\n muted\n loop\n autoPlay\n playsInline\n />\n );\n return <img className={cls} src={p.Url} alt=\"\" />;\n}\n\nfunction useBalances(): Record<string, { Amount?: number }> | undefined {\n const state = useUserState();\n return state?.InventoryV2?.VirtualCurrencies as\n Record<string, { Amount?: number }> | undefined;\n}\n\n/** \"Free\" or the price: Aspis with its icon, a game currency with its icon. */\nfunction PriceChip({ item }: { item: AppItem }): ReactNode {\n const balances = useBalances();\n const label = priceLabelOf(item, (p) => canAfford(p, balances));\n if (label.free)\n return <span className=\"iam-price iam-free\">{t(\"free\")}</span>;\n return label.option ? (\n <span className=\"iam-price\">\n <Price lines={label.option.price} />\n </span>\n ) : null;\n}\n\nfunction Price({ lines }: { lines: ResourceLine[] }): ReactNode {\n const catalog = useCatalog();\n return (\n <>\n {lines.map((line) => (\n <span key={`${line.kind}:${line.id}`} className=\"iam-price-line\">\n {line.kind === \"credits\" ? (\n <PlatformCurrencyIcon size={14} />\n ) : (\n <Icon glyph={catalog.iconOf(line)} size={14} />\n )}\n {formatLineAmount(line)}\n </span>\n ))}\n </>\n );\n}\n\n// ── One card: photos, choices, the button with the price, the result ─────────────────────────\n\n/** A photo from the device, downscaled to {@link fitSize} and sent as JPEG. null: not a picture. */\nasync function readPhoto(file: File): Promise<AIInputImage | null> {\n if (!file.type.startsWith(\"image/\")) return null;\n const bitmap = await createImageBitmap(file).catch(() => null);\n if (!bitmap) return null;\n const size = fitSize(bitmap.width, bitmap.height);\n const canvas = document.createElement(\"canvas\");\n canvas.width = size.width;\n canvas.height = size.height;\n canvas.getContext(\"2d\")?.drawImage(bitmap, 0, 0, size.width, size.height);\n bitmap.close();\n return {\n Base64: canvas.toDataURL(\"image/jpeg\", 0.9),\n ContentType: \"image/jpeg\",\n };\n}\n\n/** Starts the generation (an Aspis price is confirmed by the player on the way) and waits for the result. */\nasync function generate(\n client: IDosGamesClient,\n item: AppItem,\n files: Record<string, AIInputImage>,\n values: Record<string, string>,\n prompt: string,\n optionID: string | undefined,\n): Promise<\n { ok: true; data: AIGenerationView } | { ok: false; error: string }\n> {\n // One key for both calls of an Aspis price: the request, then the confirmed generation.\n const relatedEntityID = crypto.randomUUID();\n const opts = (creditsConfirmationID?: string) => ({\n ...(optionID ? { selectedOptionID: optionID } : {}),\n relatedEntityID,\n ...(creditsConfirmationID ? { creditsConfirmationID } : {}),\n ...(item.template ? { variables: values } : {}),\n });\n const call = (id?: string) => {\n if (item.template)\n return client.ai.generateFromTemplate(item.key, files, opts(id));\n switch (item.kind) {\n case \"Image\":\n return client.ai.generateImage(item.key, { Prompt: prompt }, opts(id));\n case \"Video\":\n return client.ai.generateVideo(item.key, { Prompt: prompt }, opts(id));\n case \"Audio\":\n return client.ai.generateAudio(item.key, { Text: prompt }, opts(id));\n case \"Music\":\n return client.ai.generateMusic(item.key, { Prompt: prompt }, opts(id));\n default:\n return client.ai.generateThreeD(item.key, { Prompt: prompt }, opts(id));\n }\n };\n const started = await payWithCredits(client, call);\n return started.ok\n ? client.ai.waitForGeneration(started.data, { timeoutMs: 15 * 60_000 })\n : started;\n}\n\nfunction Run({\n item,\n onBack,\n onDone,\n}: {\n item: AppItem;\n onBack: () => void;\n onDone: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const balances = useBalances();\n const tpl = item.template;\n const [files, setFiles] = useState<Record<string, AIInputImage>>({});\n const [values, setValues] = useState<Record<string, string>>({});\n const [prompt, setPrompt] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [result, setResult] = useState<AIGenerationView | null>(null);\n const alive = useRef(true);\n useEffect(\n () => () => {\n alive.current = false;\n },\n [],\n );\n\n const label = priceLabelOf(item, (p) => canAfford(p, balances));\n /** Nothing to pay with here (Aspis inside the iOS app): the card is only looked at, no photos and no button. */\n const viewOnly = !label.free && !label.option;\n const ready = tpl\n ? missingInputs(tpl, files).length === 0 &&\n missingValues(tpl, values).length === 0\n : !!prompt.trim();\n\n const go = async () => {\n setBusy(true);\n setResult(null);\n const res = await generate(\n client,\n item,\n files,\n values,\n prompt.trim(),\n label.free ? undefined : label.option?.id,\n );\n if (!alive.current) return;\n setBusy(false);\n onDone();\n if (!res.ok) {\n if (!isCreditsCancel(res.error)) toast(errorText(res.error), \"error\");\n return;\n }\n if (res.data.Status !== \"completed\")\n return toast(res.data.Error?.Message || t(\"failed\"), \"error\");\n setResult(res.data);\n };\n\n const name = tpl ? localized(tpl.Name, lang()) : t(\"own\");\n const download = result ? firstAssetUrl(result) : \"\";\n\n return (\n <>\n <header className=\"iam-bar\">\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"back\")}\n onClick={onBack}\n >\n <Svg d={ICON.back} />\n </button>\n <span className=\"iam-title\">{name}</span>\n <span className=\"iam-icon-btn\" aria-hidden />\n </header>\n <div className=\"iam-page\">\n {result ? (\n <Result generation={result} />\n ) : tpl ? (\n <div\n className={item.kind === \"Video\" ? \"iam-hero iam-tall\" : \"iam-hero\"}\n >\n <Preview template={tpl} cover />\n {busy ? <Working video={item.kind === \"Video\"} /> : null}\n </div>\n ) : busy ? (\n <div className=\"iam-hero\">\n <Working video={item.kind === \"Video\"} />\n </div>\n ) : null}\n\n {result ? null : tpl ? (\n <>\n {localized(tpl.Description, lang()) ? (\n <p className=\"iam-muted\">{localized(tpl.Description, lang())}</p>\n ) : null}\n {viewOnly ? null : (\n <>\n <Photos template={tpl} files={files} onChange={setFiles} />\n <Choices template={tpl} values={values} onChange={setValues} />\n </>\n )}\n </>\n ) : viewOnly ? null : (\n <textarea\n className=\"iam-input\"\n value={prompt}\n maxLength={item.maxChars > 0 ? item.maxChars : undefined}\n placeholder={item.kind === \"Audio\" ? t(\"sayWhat\") : t(\"describe\")}\n onChange={(e) => setPrompt(e.target.value)}\n rows={5}\n />\n )}\n </div>\n\n {viewOnly && !result ? null : (\n <footer className=\"iam-dock\">\n {result ? (\n <>\n {download ? (\n <a\n className=\"iam-btn\"\n href={download}\n target=\"_blank\"\n rel=\"noreferrer\"\n download\n >\n <Svg d={ICON.download} size={18} /> {t(\"download\")}\n </a>\n ) : null}\n <button\n type=\"button\"\n className=\"iam-btn iam-btn-ghost\"\n onClick={() => setResult(null)}\n >\n {t(\"again\")}\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n className=\"iam-btn\"\n disabled={!ready || busy}\n onClick={() => void go()}\n data-tutorial-anchor={`ai-media-app:${item.key}`}\n >\n {busy ? (\n <>\n <span className=\"iam-spin iam-spin-sm\" /> {t(\"working\")}\n </>\n ) : (\n <>\n {t(\"create\")}\n {label.free ? (\n <span className=\"iam-btn-price\">\n · {freeText(label.left)}\n </span>\n ) : label.option ? (\n <span className=\"iam-btn-price\">\n · <Price lines={label.option.price} />\n </span>\n ) : null}\n </>\n )}\n </button>\n )}\n </footer>\n )}\n </>\n );\n}\n\nfunction freeText(left: number): string {\n return Number.isFinite(left)\n ? t(\"freeLeft\").replace(\"{n}\", String(left))\n : t(\"free\");\n}\n\nfunction Working({ video }: { video: boolean }): ReactNode {\n return (\n <span className=\"iam-working\">\n <span className=\"iam-spin\" />\n <span>{t(\"working\")}</span>\n {video ? <span className=\"iam-muted\">{t(\"videoWait\")}</span> : null}\n </span>\n );\n}\n\nfunction Photos({\n template,\n files,\n onChange,\n}: {\n template: AIPublicTemplate;\n files: Record<string, AIInputImage>;\n onChange: (next: Record<string, AIInputImage>) => void;\n}): ReactNode {\n const toast = useToast();\n const inputs = (template.Inputs ?? []).filter((i) => i.Key);\n if (inputs.length === 0) return null;\n return (\n <div className=\"iam-stack\">\n <div\n className={inputs.length === 1 ? \"iam-photos iam-one\" : \"iam-photos\"}\n >\n {inputs.map((input) => {\n const key = input.Key!;\n const file = files[key];\n return (\n <label key={key} className=\"iam-photo\">\n {file?.Base64 ? (\n <img className=\"iam-media iam-cover\" src={file.Base64} alt=\"\" />\n ) : (\n <span className=\"iam-photo-empty\">\n <Svg d={ICON.plus} size={26} />\n <span>{localized(input.Label, lang()) || t(\"addPhoto\")}</span>\n {!input.Required ? (\n <span className=\"iam-muted\">{t(\"optional\")}</span>\n ) : null}\n </span>\n )}\n <input\n type=\"file\"\n accept=\"image/*\"\n hidden\n onChange={(e) => {\n const picked = e.target.files?.[0];\n e.target.value = \"\";\n if (!picked) return;\n void readPhoto(picked).then((img) => {\n if (!img) return toast(t(\"photoBad\"), \"error\");\n onChange({ ...files, [key]: img });\n });\n }}\n />\n </label>\n );\n })}\n </div>\n <p className=\"iam-note\">{t(\"consent\")}</p>\n </div>\n );\n}\n\nfunction Choices({\n template,\n values,\n onChange,\n}: {\n template: AIPublicTemplate;\n values: Record<string, string>;\n onChange: (next: Record<string, string>) => void;\n}): ReactNode {\n const vars = (template.Variables ?? []).filter((x) => x.Name);\n if (vars.length === 0) return null;\n return (\n <div className=\"iam-stack\">\n {vars.map((x) => {\n const name = x.Name!;\n const value = values[name] ?? x.Default ?? \"\";\n return (\n <div key={name} className=\"iam-field\">\n <span className=\"iam-label\">\n {localized(x.Label, lang()) || name}\n </span>\n {x.Type === \"Choice\" ? (\n <div className=\"iam-chips iam-chips-wrap\">\n {(x.Choices ?? []).map((c) => (\n <button\n key={c.Value ?? \"\"}\n type=\"button\"\n className={\n c.Value === value ? \"iam-chip iam-chip-on\" : \"iam-chip\"\n }\n onClick={() =>\n onChange({ ...values, [name]: c.Value ?? \"\" })\n }\n >\n {localized(c.Label, lang()) || c.Value}\n </button>\n ))}\n </div>\n ) : (\n <textarea\n className=\"iam-input\"\n value={value}\n maxLength={x.MaxLength && x.MaxLength > 0 ? x.MaxLength : 500}\n placeholder={localized(x.Placeholder, lang())}\n onChange={(e) =>\n onChange({ ...values, [name]: e.target.value })\n }\n rows={x.Multiline ? 4 : 1}\n />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction firstAssetUrl(generation: AIGenerationView): string {\n return (generation.Assets ?? []).find((a) => a.Url)?.Url ?? \"\";\n}\n\nfunction Result({ generation }: { generation: AIGenerationView }): ReactNode {\n return (\n <div className=\"iam-stack\">\n {generation.OutputText ? (\n <p className=\"iam-text\">{generation.OutputText}</p>\n ) : null}\n {(generation.Assets ?? []).map((a, i) => {\n const kind = resultKind(a.ContentType);\n const url = a.Url ?? \"\";\n return kind === \"image\" ? (\n <img key={i} className=\"iam-result\" src={url} alt=\"\" />\n ) : kind === \"audio\" ? (\n <audio key={i} src={url} controls className=\"iam-audio\" />\n ) : kind === \"video\" ? (\n <video\n key={i}\n className=\"iam-result\"\n src={url}\n controls\n autoPlay\n loop\n playsInline\n />\n ) : null;\n })}\n </div>\n );\n}\n\n// ── My works: a grid of what the player made ─────────────────────────────────────────────────\n\nfunction Works({\n items,\n onBack,\n}: {\n items: AppItem[];\n onBack: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const [list, setList] = useState<AIGenerationView[] | null>(null);\n const [open, setOpen] = useState<AIGenerationView | null>(null);\n useEffect(() => {\n void client.ai\n .listGenerations({ Limit: 30 })\n .then((r) => setList(r.ok ? (r.data.Data ?? []) : []));\n }, [client]);\n\n const nameOf = (key: string | null | undefined) => {\n const item = items.find((i) => i.key === key);\n return item?.template\n ? localized(item.template.Name, lang())\n : item\n ? t(\"own\")\n : (key ?? \"\");\n };\n const download = open ? firstAssetUrl(open) : \"\";\n\n return (\n <>\n <header className=\"iam-bar\">\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"back\")}\n onClick={open ? () => setOpen(null) : onBack}\n >\n <Svg d={ICON.back} />\n </button>\n <span className=\"iam-title\">\n {open ? nameOf(open.FeatureKey) : t(\"history\")}\n </span>\n <span className=\"iam-icon-btn\" aria-hidden />\n </header>\n {open ? (\n <>\n <div className=\"iam-page\">\n <Result generation={open} />\n </div>\n {download ? (\n <footer className=\"iam-dock\">\n <a\n className=\"iam-btn\"\n href={download}\n target=\"_blank\"\n rel=\"noreferrer\"\n download\n >\n <Svg d={ICON.download} size={18} /> {t(\"download\")}\n </a>\n </footer>\n ) : null}\n </>\n ) : !list ? (\n <div className=\"iam-center\">\n <span className=\"iam-spin\" />\n </div>\n ) : list.length === 0 ? (\n <p className=\"iam-empty\">{t(\"noHistory\")}</p>\n ) : (\n <div className=\"iam-grid\">\n {list.map((g) => {\n const asset = (g.Assets ?? []).find((a) => a.Url);\n const kind = resultKind(asset?.ContentType);\n const done = g.Status === \"completed\";\n return (\n <button\n key={g.GenerationID}\n type=\"button\"\n className=\"iam-card\"\n disabled={!done}\n onClick={() => setOpen(g)}\n >\n {done && asset?.Url && kind === \"image\" ? (\n <img className=\"iam-media iam-cover\" src={asset.Url} alt=\"\" />\n ) : done && asset?.Url && kind === \"video\" ? (\n <video\n className=\"iam-media iam-cover\"\n src={asset.Url}\n muted\n playsInline\n preload=\"metadata\"\n />\n ) : (\n <span className=\"iam-own\">\n {g.Status === \"failed\" ? (\n <span className=\"iam-muted\">{t(\"s_failed\")}</span>\n ) : done ? (\n <Svg d={ICON.bot} size={28} />\n ) : (\n <span className=\"iam-spin\" />\n )}\n </span>\n )}\n <span className=\"iam-card-foot\">\n <span className=\"iam-card-name\">{nameOf(g.FeatureKey)}</span>\n </span>\n </button>\n );\n })}\n </div>\n )}\n </>\n );\n}\n\n// ── Icons (lucide shapes, drawn with the current color) ──────────────────────────────────────\n\nconst ICON = {\n back: \"M19 12H5M12 19l-7-7 7-7\",\n works: \"M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z\",\n plus: \"M12 5v14M5 12h14\",\n pen: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n download: \"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\",\n bot: \"M12 8V4H8M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2ZM2 14h2M20 14h2M15 13v2M9 13v2\",\n} as const;\n\nfunction Svg({ d, size = 22 }: { d: string; size?: number }): ReactNode {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n >\n <path d={d} />\n </svg>\n );\n}\n"
50
+ "content": "import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport type {\n AIGenerationView,\n AIInputImage,\n AIPublicDefinitions,\n AIPublicSetting,\n AIPublicTemplate,\n AITemplatePrice,\n IDosGamesClient,\n} from \"@idosgames/core\";\nimport { canPayWithPlatformCredits } from \"@idosgames/core\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n canAfford,\n errorText,\n formatLineAmount,\n Icon,\n isCreditsCancel,\n payWithCredits,\n pickLanguage,\n PlatformCurrencyIcon,\n useCatalog,\n useToast,\n type ResourceLine,\n} from \"@idosgames/react/ui\";\nimport {\n appItems,\n changedSettings,\n fitSize,\n kindsOf,\n localized,\n missingInputs,\n missingValues,\n priceLabelOf,\n repriced,\n resultKind,\n type AppItem,\n type Kind,\n} from \"./model\";\nimport { t } from \"./i18n\";\nimport { APP_CSS, FONT_URL } from \"./styles\";\n\n// The AI generator app: the title's AI features as cards (trends and templates of the platform AI studio, and\n// \"describe it yourself\" for a kind with a plain feature). Prices, free generations and the cards come from the\n// engine's sanitized AI definitions; the template's prompt never reaches the player.\n//\n// The look is idosgames.com, not a game (owner 09.11.2026): dark zinc, the site's blue, Onest, flat cards; and as few\n// buttons as possible: a card → a photo → ONE button with the price → the result with Download and Again.\n\ntype Screen = { at: \"home\" } | { at: \"run\"; key: string } | { at: \"works\" };\n\n/**\n * The AI definitions of the title: read once, read again after a generation (the free count moves). Always from the\n * engine (`forceRefresh`), never the cached config section: only the engine's answer carries the template cards\n * (`PlatformTemplate`) and the player's `FreeLeftToday`; the config copy has neither, and every template card would\n * be dropped as \"taken off the site\".\n */\nfunction useDefinitions(client: IDosGamesClient): {\n defs: AIPublicDefinitions | null;\n refresh: () => void;\n} {\n const [defs, setDefs] = useState<AIPublicDefinitions | null>(null);\n const refresh = useCallback(() => {\n void client.ai\n .getDefinitions({ forceRefresh: true })\n .then((r) => setDefs(r.ok ? r.data : { Enabled: false }));\n }, [client]);\n useEffect(refresh, [refresh]);\n return { defs, refresh };\n}\n\n/** The site's font, once per page. */\nfunction useSiteFont(): void {\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n if (document.querySelector(`link[href=\"${FONT_URL}\"]`)) return;\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = FONT_URL;\n document.head.appendChild(link);\n }, []);\n}\n\nexport function MediaApp(): ReactNode {\n useSiteFont();\n const client = useIDosGamesClient();\n const { defs, refresh } = useDefinitions(client);\n // In the iOS app Aspis cannot be paid (App Store 3.1.1): its prices are dropped, those cards only show the preview.\n const items = appItems(defs, canPayWithPlatformCredits());\n const kinds = kindsOf(items);\n const [kind, setKind] = useState<Kind | null>(null);\n const [screen, setScreen] = useState<Screen>({ at: \"home\" });\n const home = () => setScreen({ at: \"home\" });\n\n const current = kind && kinds.includes(kind) ? kind : kinds[0];\n const item =\n screen.at === \"run\" ? items.find((i) => i.key === screen.key) : undefined;\n\n return (\n <div className=\"iam\">\n <style>{APP_CSS}</style>\n {!defs ? (\n <div className=\"iam-center\">\n <span className=\"iam-spin\" />\n </div>\n ) : item ? (\n <Run key={item.key} item={item} onBack={home} onDone={refresh} />\n ) : screen.at === \"works\" ? (\n <Works items={items} onBack={home} />\n ) : (\n <>\n <header className=\"iam-bar\">\n <span className=\"iam-title\">{appTitle()}</span>\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"history\")}\n onClick={() => setScreen({ at: \"works\" })}\n >\n <Svg d={ICON.works} />\n </button>\n </header>\n {kinds.length > 1 ? (\n <nav className=\"iam-chips\">\n {kinds.map((k) => (\n <button\n key={k}\n type=\"button\"\n className={\n k === current ? \"iam-chip iam-chip-on\" : \"iam-chip\"\n }\n onClick={() => setKind(k)}\n >\n {t(`k_${k}`)}\n </button>\n ))}\n </nav>\n ) : null}\n {current ? (\n <Grid\n items={items.filter((i) => i.kind === current)}\n onOpen={(key) => setScreen({ at: \"run\", key })}\n />\n ) : (\n <p className=\"iam-empty\">{t(\"empty\")}</p>\n )}\n </>\n )}\n </div>\n );\n}\n\n/** The app's name: the page title the author set, else \"AI generator\". */\nfunction appTitle(): string {\n const title = typeof document === \"undefined\" ? \"\" : document.title.trim();\n return title || t(\"title\");\n}\n\n// ── Home: one grid of big cards, trends first ────────────────────────────────────────────────\n\nfunction Grid({\n items,\n onOpen,\n}: {\n items: AppItem[];\n onOpen: (key: string) => void;\n}): ReactNode {\n // Trends first, then templates, \"describe it yourself\" last: a ready card converts better than an empty box.\n const ordered = [\n ...items.filter((i) => i.trend),\n ...items.filter((i) => i.template && !i.trend),\n ...items.filter((i) => !i.template),\n ];\n return (\n <div className=\"iam-grid\">\n {ordered.map((i) => (\n <button\n key={i.key}\n type=\"button\"\n className={i.kind === \"Video\" ? \"iam-card iam-tall\" : \"iam-card\"}\n onClick={() => onOpen(i.key)}\n >\n {i.template ? (\n <Preview template={i.template} cover />\n ) : (\n <span className=\"iam-own\">\n <Svg d={ICON.pen} size={28} />\n </span>\n )}\n {i.trend ? <span className=\"iam-badge\">{t(\"trend\")}</span> : null}\n <span className=\"iam-card-foot\">\n <span className=\"iam-card-name\">\n {i.template ? localized(i.template.Name, lang()) : t(\"own\")}\n </span>\n <PriceChip item={i} />\n </span>\n </button>\n ))}\n </div>\n );\n}\n\nconst lang = (): string => pickLanguage();\n\n/** The sample of a template: a silent looping video or a picture; the robot when there is none. */\nfunction Preview({\n template,\n cover,\n}: {\n template: AIPublicTemplate;\n cover?: boolean;\n}): ReactNode {\n const p = template.Preview;\n const cls = cover ? \"iam-media iam-cover\" : \"iam-media\";\n const type = (p?.Type ?? \"\").toLowerCase();\n if (!p?.Url || type === \"audio\")\n return (\n <span className=\"iam-own\">\n <Svg d={ICON.bot} size={32} />\n </span>\n );\n if (type === \"video\")\n return (\n <video\n className={cls}\n src={p.Url}\n poster={p.PosterUrl ?? undefined}\n muted\n loop\n autoPlay\n playsInline\n />\n );\n return <img className={cls} src={p.Url} alt=\"\" />;\n}\n\nfunction useBalances(): Record<string, { Amount?: number }> | undefined {\n const state = useUserState();\n return state?.InventoryV2?.VirtualCurrencies as\n Record<string, { Amount?: number }> | undefined;\n}\n\n/** \"Free\" or the price: Aspis with its icon, a game currency with its icon. */\nfunction PriceChip({ item }: { item: AppItem }): ReactNode {\n const balances = useBalances();\n const label = priceLabelOf(item, (p) => canAfford(p, balances));\n if (label.free)\n return <span className=\"iam-price iam-free\">{t(\"free\")}</span>;\n return label.option ? (\n <span className=\"iam-price\">\n <Price lines={label.option.price} />\n </span>\n ) : null;\n}\n\nfunction Price({ lines }: { lines: ResourceLine[] }): ReactNode {\n const catalog = useCatalog();\n return (\n <>\n {lines.map((line) => (\n <span key={`${line.kind}:${line.id}`} className=\"iam-price-line\">\n {line.kind === \"credits\" ? (\n <PlatformCurrencyIcon size={14} />\n ) : (\n <Icon glyph={catalog.iconOf(line)} size={14} />\n )}\n {formatLineAmount(line)}\n </span>\n ))}\n </>\n );\n}\n\n// ── One card: photos, choices, the button with the price, the result ─────────────────────────\n\n/** A photo from the device, downscaled to {@link fitSize} and sent as JPEG. null: not a picture. */\nasync function readPhoto(file: File): Promise<AIInputImage | null> {\n if (!file.type.startsWith(\"image/\")) return null;\n const bitmap = await createImageBitmap(file).catch(() => null);\n if (!bitmap) return null;\n const size = fitSize(bitmap.width, bitmap.height);\n const canvas = document.createElement(\"canvas\");\n canvas.width = size.width;\n canvas.height = size.height;\n canvas.getContext(\"2d\")?.drawImage(bitmap, 0, 0, size.width, size.height);\n bitmap.close();\n return {\n Base64: canvas.toDataURL(\"image/jpeg\", 0.9),\n ContentType: \"image/jpeg\",\n };\n}\n\n/** Starts the generation (an Aspis price is confirmed by the player on the way) and waits for the result. */\nasync function generate(\n client: IDosGamesClient,\n item: AppItem,\n files: Record<string, AIInputImage>,\n values: Record<string, string>,\n prompt: string,\n optionID: string | undefined,\n /** Settings the player changed: the same values go with the confirmed call (the Aspis price is for them). */\n params: Record<string, string>,\n): Promise<\n { ok: true; data: AIGenerationView } | { ok: false; error: string }\n> {\n // One key for both calls of an Aspis price: the request, then the confirmed generation.\n const relatedEntityID = crypto.randomUUID();\n const opts = (creditsConfirmationID?: string) => ({\n ...(optionID ? { selectedOptionID: optionID } : {}),\n relatedEntityID,\n ...(creditsConfirmationID ? { creditsConfirmationID } : {}),\n ...(item.template ? { variables: values } : {}),\n ...(item.template && Object.keys(params).length > 0 ? { params } : {}),\n });\n const call = (id?: string) => {\n if (item.template)\n return client.ai.generateFromTemplate(item.key, files, opts(id));\n switch (item.kind) {\n case \"Image\":\n return client.ai.generateImage(item.key, { Prompt: prompt }, opts(id));\n case \"Video\":\n return client.ai.generateVideo(item.key, { Prompt: prompt }, opts(id));\n case \"Audio\":\n return client.ai.generateAudio(item.key, { Text: prompt }, opts(id));\n case \"Music\":\n return client.ai.generateMusic(item.key, { Prompt: prompt }, opts(id));\n default:\n return client.ai.generateThreeD(item.key, { Prompt: prompt }, opts(id));\n }\n };\n const started = await payWithCredits(client, call);\n return started.ok\n ? client.ai.waitForGeneration(started.data, { timeoutMs: 15 * 60_000 })\n : started;\n}\n\nfunction Run({\n item,\n onBack,\n onDone,\n}: {\n item: AppItem;\n onBack: () => void;\n onDone: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const balances = useBalances();\n const tpl = item.template;\n const [files, setFiles] = useState<Record<string, AIInputImage>>({});\n const [values, setValues] = useState<Record<string, string>>({});\n const [prompt, setPrompt] = useState(\"\");\n // Settings the author gave to the player: what the player typed or picked; the price follows them.\n const [settings, setSettings] = useState<Record<string, string>>({});\n const changed = changedSettings(tpl, settings);\n const changedKey = JSON.stringify(changed);\n // data null: the server could not price this choice (the button stays off until the player changes it again).\n const [price, setPrice] = useState<{\n key: string;\n data: AITemplatePrice | null;\n } | null>(null);\n const [busy, setBusy] = useState(false);\n const [result, setResult] = useState<AIGenerationView | null>(null);\n const alive = useRef(true);\n useEffect(\n () => () => {\n alive.current = false;\n },\n [],\n );\n\n // The author's settings: the card's price. Changed ones: the server's price for them (asked after a short pause).\n useEffect(() => {\n if (changedKey === \"{}\") return;\n let live = true;\n const timer = setTimeout(() => {\n void client.ai\n .estimateTemplatePrice(item.key, JSON.parse(changedKey), values)\n .then((r) => {\n if (live) setPrice({ key: changedKey, data: r.ok ? r.data : null });\n });\n }, 350);\n return () => {\n live = false;\n clearTimeout(timer);\n };\n }, [client, item.key, changedKey]);\n const pricing = changedKey !== \"{}\" && price?.key !== changedKey;\n const unpriced =\n changedKey !== \"{}\" && price?.key === changedKey && !price.data;\n const priced =\n changedKey !== \"{}\" && price?.key === changedKey && price.data\n ? repriced(item, price.data)\n : item;\n const label = priceLabelOf(priced, (p) => canAfford(p, balances));\n /** Nothing to pay with here (Aspis inside the iOS app): the card is only looked at, no photos and no button. */\n const viewOnly = !label.free && !label.option;\n const ready = tpl\n ? missingInputs(tpl, files).length === 0 &&\n missingValues(tpl, values).length === 0\n : !!prompt.trim();\n\n const go = async () => {\n setBusy(true);\n setResult(null);\n const res = await generate(\n client,\n item,\n files,\n values,\n prompt.trim(),\n label.free ? undefined : label.option?.id,\n changed,\n );\n if (!alive.current) return;\n setBusy(false);\n onDone();\n if (!res.ok) {\n if (!isCreditsCancel(res.error)) toast(errorText(res.error), \"error\");\n return;\n }\n if (res.data.Status !== \"completed\")\n return toast(res.data.Error?.Message || t(\"failed\"), \"error\");\n setResult(res.data);\n };\n\n const name = tpl ? localized(tpl.Name, lang()) : t(\"own\");\n const download = result ? firstAssetUrl(result) : \"\";\n\n return (\n <>\n <header className=\"iam-bar\">\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"back\")}\n onClick={onBack}\n >\n <Svg d={ICON.back} />\n </button>\n <span className=\"iam-title\">{name}</span>\n <span className=\"iam-icon-btn\" aria-hidden />\n </header>\n <div className=\"iam-page\">\n {result ? (\n <Result generation={result} />\n ) : tpl ? (\n <div\n className={item.kind === \"Video\" ? \"iam-hero iam-tall\" : \"iam-hero\"}\n >\n <Preview template={tpl} cover />\n {busy ? <Working video={item.kind === \"Video\"} /> : null}\n </div>\n ) : busy ? (\n <div className=\"iam-hero\">\n <Working video={item.kind === \"Video\"} />\n </div>\n ) : null}\n\n {result ? null : tpl ? (\n <>\n {localized(tpl.Description, lang()) ? (\n <p className=\"iam-muted\">{localized(tpl.Description, lang())}</p>\n ) : null}\n {viewOnly ? null : (\n <>\n <Photos template={tpl} files={files} onChange={setFiles} />\n <Choices template={tpl} values={values} onChange={setValues} />\n <Settings\n template={tpl}\n values={settings}\n onChange={setSettings}\n />\n </>\n )}\n </>\n ) : viewOnly ? null : (\n <textarea\n className=\"iam-input\"\n value={prompt}\n maxLength={item.maxChars > 0 ? item.maxChars : undefined}\n placeholder={item.kind === \"Audio\" ? t(\"sayWhat\") : t(\"describe\")}\n onChange={(e) => setPrompt(e.target.value)}\n rows={5}\n />\n )}\n </div>\n\n {viewOnly && !result ? null : (\n <footer className=\"iam-dock\">\n {result ? (\n <>\n {download ? (\n <a\n className=\"iam-btn\"\n href={download}\n target=\"_blank\"\n rel=\"noreferrer\"\n download\n >\n <Svg d={ICON.download} size={18} /> {t(\"download\")}\n </a>\n ) : null}\n <button\n type=\"button\"\n className=\"iam-btn iam-btn-ghost\"\n onClick={() => setResult(null)}\n >\n {t(\"again\")}\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n className=\"iam-btn\"\n disabled={!ready || busy || pricing || unpriced}\n onClick={() => void go()}\n data-tutorial-anchor={`ai-media-app:${item.key}`}\n >\n {busy ? (\n <>\n <span className=\"iam-spin iam-spin-sm\" /> {t(\"working\")}\n </>\n ) : (\n <>\n {t(\"create\")}\n {pricing ? (\n <span className=\"iam-btn-price\">\n · <span className=\"iam-spin iam-spin-sm\" />\n </span>\n ) : unpriced ? (\n <span className=\"iam-btn-price\">· {t(\"noPrice\")}</span>\n ) : label.free ? (\n <span className=\"iam-btn-price\">\n · {freeText(label.left)}\n </span>\n ) : label.option ? (\n <span className=\"iam-btn-price\">\n · <Price lines={label.option.price} />\n </span>\n ) : null}\n </>\n )}\n </button>\n )}\n </footer>\n )}\n </>\n );\n}\n\nfunction freeText(left: number): string {\n return Number.isFinite(left)\n ? t(\"freeLeft\").replace(\"{n}\", String(left))\n : t(\"free\");\n}\n\nfunction Working({ video }: { video: boolean }): ReactNode {\n return (\n <span className=\"iam-working\">\n <span className=\"iam-spin\" />\n <span>{t(\"working\")}</span>\n {video ? <span className=\"iam-muted\">{t(\"videoWait\")}</span> : null}\n </span>\n );\n}\n\nfunction Photos({\n template,\n files,\n onChange,\n}: {\n template: AIPublicTemplate;\n files: Record<string, AIInputImage>;\n onChange: (next: Record<string, AIInputImage>) => void;\n}): ReactNode {\n const toast = useToast();\n const inputs = (template.Inputs ?? []).filter((i) => i.Key);\n if (inputs.length === 0) return null;\n return (\n <div className=\"iam-stack\">\n <div\n className={inputs.length === 1 ? \"iam-photos iam-one\" : \"iam-photos\"}\n >\n {inputs.map((input) => {\n const key = input.Key!;\n const file = files[key];\n return (\n <label key={key} className=\"iam-photo\">\n {file?.Base64 ? (\n <img className=\"iam-media iam-cover\" src={file.Base64} alt=\"\" />\n ) : (\n <span className=\"iam-photo-empty\">\n <Svg d={ICON.plus} size={26} />\n <span>{localized(input.Label, lang()) || t(\"addPhoto\")}</span>\n {!input.Required ? (\n <span className=\"iam-muted\">{t(\"optional\")}</span>\n ) : null}\n </span>\n )}\n <input\n type=\"file\"\n accept=\"image/*\"\n hidden\n onChange={(e) => {\n const picked = e.target.files?.[0];\n e.target.value = \"\";\n if (!picked) return;\n void readPhoto(picked).then((img) => {\n if (!img) return toast(t(\"photoBad\"), \"error\");\n onChange({ ...files, [key]: img });\n });\n }}\n />\n </label>\n );\n })}\n </div>\n <p className=\"iam-note\">{t(\"consent\")}</p>\n </div>\n );\n}\n\nfunction Choices({\n template,\n values,\n onChange,\n}: {\n template: AIPublicTemplate;\n values: Record<string, string>;\n onChange: (next: Record<string, string>) => void;\n}): ReactNode {\n const vars = (template.Variables ?? []).filter((x) => x.Name);\n if (vars.length === 0) return null;\n return (\n <div className=\"iam-stack\">\n {vars.map((x) => {\n const name = x.Name!;\n const value = values[name] ?? x.Default ?? \"\";\n return (\n <div key={name} className=\"iam-field\">\n <span className=\"iam-label\">\n {localized(x.Label, lang()) || name}\n </span>\n {x.Type === \"Choice\" ? (\n <div className=\"iam-chips iam-chips-wrap\">\n {(x.Choices ?? []).map((c) => (\n <button\n key={c.Value ?? \"\"}\n type=\"button\"\n className={\n c.Value === value ? \"iam-chip iam-chip-on\" : \"iam-chip\"\n }\n onClick={() =>\n onChange({ ...values, [name]: c.Value ?? \"\" })\n }\n >\n {localized(c.Label, lang()) || c.Value}\n </button>\n ))}\n </div>\n ) : (\n <textarea\n className=\"iam-input\"\n value={value}\n maxLength={x.MaxLength && x.MaxLength > 0 ? x.MaxLength : 500}\n placeholder={localized(x.Placeholder, lang())}\n onChange={(e) =>\n onChange({ ...values, [name]: e.target.value })\n }\n rows={x.Multiline ? 4 : 1}\n />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\n/**\n * The generation settings the author gave to the player, inside the author's range; each starts from the author's\n * value. Lists are chips, numbers a field with the range, switches Yes / No.\n */\nfunction Settings({\n template,\n values,\n onChange,\n}: {\n template: AIPublicTemplate;\n values: Record<string, string>;\n onChange: (next: Record<string, string>) => void;\n}): ReactNode {\n const list = (template.Settings ?? []).filter((x) => x.Key);\n if (list.length === 0) return null;\n const set = (key: string, v: string) => onChange({ ...values, [key]: v });\n return (\n <div className=\"iam-stack\">\n <span className=\"iam-label\">{t(\"settings\")}</span>\n {list.map((x) => (\n <SettingRow\n key={x.Key}\n setting={x}\n value={values[x.Key] ?? x.Default ?? \"\"}\n onChange={(v) => set(x.Key, v)}\n />\n ))}\n </div>\n );\n}\n\nfunction SettingRow({\n setting: x,\n value,\n onChange,\n}: {\n setting: AIPublicSetting;\n value: string;\n onChange: (v: string) => void;\n}): ReactNode {\n const unit = localized(x.Unit, lang());\n const hint = localized(x.Hint, lang());\n const name = localized(x.Name, lang()) || x.Key;\n const suffix = unit ? \" \" + unit : \"\";\n const chip = (v: string, text: string) => (\n <button\n key={v}\n type=\"button\"\n className={v === value ? \"iam-chip iam-chip-on\" : \"iam-chip\"}\n onClick={() => onChange(v)}\n >\n {text}\n </button>\n );\n const range =\n x.Kind === \"Number\" && (x.Min != null || x.Max != null)\n ? String(x.Min ?? \"\") + \" - \" + String(x.Max ?? \"\") + suffix\n : undefined;\n return (\n <div className=\"iam-field\">\n <span className=\"iam-muted\" title={hint || undefined}>\n {name}\n </span>\n {x.Kind === \"Select\" ? (\n <div className=\"iam-chips iam-chips-wrap\">\n {(x.Choices ?? []).map((c) =>\n chip(c.Value, (localized(c.Name, lang()) || c.Value) + suffix),\n )}\n </div>\n ) : x.Kind === \"Toggle\" ? (\n <div className=\"iam-chips iam-chips-wrap\">\n {chip(\"true\", t(\"yes\"))}\n {chip(\"false\", t(\"no\"))}\n </div>\n ) : (\n <input\n className=\"iam-input\"\n type={x.Kind === \"Number\" ? \"number\" : \"text\"}\n inputMode={x.Kind === \"Number\" ? \"numeric\" : undefined}\n value={value}\n min={x.Min ?? undefined}\n max={x.Max ?? undefined}\n step={x.Step ?? undefined}\n placeholder={range}\n onChange={(e) => onChange(e.target.value)}\n />\n )}\n </div>\n );\n}\n\nfunction firstAssetUrl(generation: AIGenerationView): string {\n return (generation.Assets ?? []).find((a) => a.Url)?.Url ?? \"\";\n}\n\nfunction Result({ generation }: { generation: AIGenerationView }): ReactNode {\n return (\n <div className=\"iam-stack\">\n {generation.OutputText ? (\n <p className=\"iam-text\">{generation.OutputText}</p>\n ) : null}\n {(generation.Assets ?? []).map((a, i) => {\n const kind = resultKind(a.ContentType);\n const url = a.Url ?? \"\";\n return kind === \"image\" ? (\n <img key={i} className=\"iam-result\" src={url} alt=\"\" />\n ) : kind === \"audio\" ? (\n <audio key={i} src={url} controls className=\"iam-audio\" />\n ) : kind === \"video\" ? (\n <video\n key={i}\n className=\"iam-result\"\n src={url}\n controls\n autoPlay\n loop\n playsInline\n />\n ) : null;\n })}\n </div>\n );\n}\n\n// ── My works: a grid of what the player made ─────────────────────────────────────────────────\n\nfunction Works({\n items,\n onBack,\n}: {\n items: AppItem[];\n onBack: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const [list, setList] = useState<AIGenerationView[] | null>(null);\n const [open, setOpen] = useState<AIGenerationView | null>(null);\n useEffect(() => {\n void client.ai\n .listGenerations({ Limit: 30 })\n .then((r) => setList(r.ok ? (r.data.Data ?? []) : []));\n }, [client]);\n\n const nameOf = (key: string | null | undefined) => {\n const item = items.find((i) => i.key === key);\n return item?.template\n ? localized(item.template.Name, lang())\n : item\n ? t(\"own\")\n : (key ?? \"\");\n };\n const download = open ? firstAssetUrl(open) : \"\";\n\n return (\n <>\n <header className=\"iam-bar\">\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"back\")}\n onClick={open ? () => setOpen(null) : onBack}\n >\n <Svg d={ICON.back} />\n </button>\n <span className=\"iam-title\">\n {open ? nameOf(open.FeatureKey) : t(\"history\")}\n </span>\n <span className=\"iam-icon-btn\" aria-hidden />\n </header>\n {open ? (\n <>\n <div className=\"iam-page\">\n <Result generation={open} />\n </div>\n {download ? (\n <footer className=\"iam-dock\">\n <a\n className=\"iam-btn\"\n href={download}\n target=\"_blank\"\n rel=\"noreferrer\"\n download\n >\n <Svg d={ICON.download} size={18} /> {t(\"download\")}\n </a>\n </footer>\n ) : null}\n </>\n ) : !list ? (\n <div className=\"iam-center\">\n <span className=\"iam-spin\" />\n </div>\n ) : list.length === 0 ? (\n <p className=\"iam-empty\">{t(\"noHistory\")}</p>\n ) : (\n <div className=\"iam-grid\">\n {list.map((g) => {\n const asset = (g.Assets ?? []).find((a) => a.Url);\n const kind = resultKind(asset?.ContentType);\n const done = g.Status === \"completed\";\n return (\n <button\n key={g.GenerationID}\n type=\"button\"\n className=\"iam-card\"\n disabled={!done}\n onClick={() => setOpen(g)}\n >\n {done && asset?.Url && kind === \"image\" ? (\n <img className=\"iam-media iam-cover\" src={asset.Url} alt=\"\" />\n ) : done && asset?.Url && kind === \"video\" ? (\n <video\n className=\"iam-media iam-cover\"\n src={asset.Url}\n muted\n playsInline\n preload=\"metadata\"\n />\n ) : (\n <span className=\"iam-own\">\n {g.Status === \"failed\" ? (\n <span className=\"iam-muted\">{t(\"s_failed\")}</span>\n ) : done ? (\n <Svg d={ICON.bot} size={28} />\n ) : (\n <span className=\"iam-spin\" />\n )}\n </span>\n )}\n <span className=\"iam-card-foot\">\n <span className=\"iam-card-name\">{nameOf(g.FeatureKey)}</span>\n </span>\n </button>\n );\n })}\n </div>\n )}\n </>\n );\n}\n\n// ── Icons (lucide shapes, drawn with the current color) ──────────────────────────────────────\n\nconst ICON = {\n back: \"M19 12H5M12 19l-7-7 7-7\",\n works: \"M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z\",\n plus: \"M12 5v14M5 12h14\",\n pen: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n download: \"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\",\n bot: \"M12 8V4H8M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2ZM2 14h2M20 14h2M15 13v2M9 13v2\",\n} as const;\n\nfunction Svg({ d, size = 22 }: { d: string; size?: number }): ReactNode {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n >\n <path d={d} />\n </svg>\n );\n}\n"
51
51
  },
52
52
  {
53
53
  "path": "i18n.ts",
54
- "content": "import { makeT } from \"@idosgames/react/ui\";\n\nconst EN = {\n title: \"AI generator\",\n k_Video: \"Video\",\n k_Image: \"Pictures\",\n k_Audio: \"Voice\",\n k_Music: \"Music\",\n k_ThreeD: \"3D\",\n history: \"My works\",\n trend: \"Trend\",\n own: \"Describe it yourself\",\n describe: \"Describe what you want…\",\n sayWhat: \"Text to say…\",\n back: \"Back\",\n addPhoto: \"Add a photo\",\n optional: \"optional\",\n consent: \"Upload only your own photos or photos of people who agreed to it.\",\n create: \"Create\",\n free: \"Free\",\n freeLeft: \"{n} free today\",\n working: \"Creating…\",\n videoWait:\n \"A video takes a few minutes. You can leave: it will be in My works.\",\n failed: \"Could not create it. Try again.\",\n download: \"Download\",\n again: \"Again\",\n empty: \"The author has not added anything to create yet.\",\n noHistory: \"Nothing created yet.\",\n photoBad: \"This file is not a picture.\",\n s_failed: \"failed\",\n} as const;\n\nexport const t = makeT<keyof typeof EN>(EN, {\n title: \"ИИ-генератор\",\n k_Video: \"Видео\",\n k_Image: \"Картинки\",\n k_Audio: \"Голос\",\n k_Music: \"Музыка\",\n k_ThreeD: \"3D\",\n history: \"Мои работы\",\n trend: \"Тренд\",\n own: \"Своё описание\",\n describe: \"Опишите, что хотите получить…\",\n sayWhat: \"Текст, который нужно озвучить…\",\n back: \"Назад\",\n addPhoto: \"Добавить фото\",\n optional: \"необязательно\",\n consent:\n \"Загружайте только свои фото или фото людей, которые на это согласны.\",\n create: \"Создать\",\n free: \"Бесплатно\",\n freeLeft: \"Сегодня бесплатно: {n}\",\n working: \"Создаём…\",\n videoWait:\n \"Ролик готовится несколько минут. Можно закрыть: он появится в «Мои работы».\",\n failed: \"Не получилось. Попробуйте ещё раз.\",\n download: \"Скачать\",\n again: \"Ещё раз\",\n empty: \"Автор пока ничего не добавил для создания.\",\n noHistory: \"Вы ещё ничего не создали.\",\n photoBad: \"Этот файл не картинка.\",\n s_failed: \"не вышло\",\n});\n"
54
+ "content": "import { makeT } from \"@idosgames/react/ui\";\n\nconst EN = {\n title: \"AI generator\",\n k_Video: \"Video\",\n k_Image: \"Pictures\",\n k_Audio: \"Voice\",\n k_Music: \"Music\",\n k_ThreeD: \"3D\",\n history: \"My works\",\n trend: \"Trend\",\n own: \"Describe it yourself\",\n describe: \"Describe what you want…\",\n sayWhat: \"Text to say…\",\n back: \"Back\",\n addPhoto: \"Add a photo\",\n optional: \"optional\",\n consent: \"Upload only your own photos or photos of people who agreed to it.\",\n create: \"Create\",\n free: \"Free\",\n freeLeft: \"{n} free today\",\n working: \"Creating…\",\n videoWait:\n \"A video takes a few minutes. You can leave: it will be in My works.\",\n failed: \"Could not create it. Try again.\",\n download: \"Download\",\n again: \"Again\",\n empty: \"The author has not added anything to create yet.\",\n noHistory: \"Nothing created yet.\",\n photoBad: \"This file is not a picture.\",\n s_failed: \"failed\",\n settings: \"Settings\",\n yes: \"Yes\",\n no: \"No\",\n noPrice: \"no price for these settings\",\n} as const;\n\nexport const t = makeT<keyof typeof EN>(EN, {\n title: \"ИИ-генератор\",\n k_Video: \"Видео\",\n k_Image: \"Картинки\",\n k_Audio: \"Голос\",\n k_Music: \"Музыка\",\n k_ThreeD: \"3D\",\n history: \"Мои работы\",\n trend: \"Тренд\",\n own: \"Своё описание\",\n describe: \"Опишите, что хотите получить…\",\n sayWhat: \"Текст, который нужно озвучить…\",\n back: \"Назад\",\n addPhoto: \"Добавить фото\",\n optional: \"необязательно\",\n consent:\n \"Загружайте только свои фото или фото людей, которые на это согласны.\",\n create: \"Создать\",\n free: \"Бесплатно\",\n freeLeft: \"Сегодня бесплатно: {n}\",\n working: \"Создаём…\",\n videoWait:\n \"Ролик готовится несколько минут. Можно закрыть: он появится в «Мои работы».\",\n failed: \"Не получилось. Попробуйте ещё раз.\",\n download: \"Скачать\",\n again: \"Ещё раз\",\n empty: \"Автор пока ничего не добавил для создания.\",\n noHistory: \"Вы ещё ничего не создали.\",\n photoBad: \"Этот файл не картинка.\",\n s_failed: \"не вышло\",\n settings: \"Настройки\",\n yes: \"Да\",\n no: \"Нет\",\n noPrice: \"нет цены для этих настроек\",\n});\n"
55
55
  },
56
56
  {
57
57
  "path": "index.ts",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  {
61
61
  "path": "model.ts",
62
- "content": "// Pure helpers of the AI generator app: covered by model.test.ts. The app is the title's AI section: the server\n// sends the features sanitized (no prompts, no models), each with its price, its free generations and, for a\n// platform template, the template's card.\n\nimport type {\n AIPublicDefinitions,\n AIPublicFeature,\n AIPublicTemplate,\n AIInputImage,\n} from \"@idosgames/core\";\nimport { costLines, type ResourceLine } from \"@idosgames/react/ui\";\n\n/** What the app makes. Text features are for the game's own code, not for this app. */\nexport type Kind = \"Video\" | \"Image\" | \"Audio\" | \"Music\" | \"ThreeD\";\n\n/** Tab order: video first (trends live there), then pictures, voice, music, 3D. */\nexport const KINDS: readonly Kind[] = [\n \"Video\",\n \"Image\",\n \"Audio\",\n \"Music\",\n \"ThreeD\",\n];\n\n/** A feature kept for the game's own code (NPC lines and the like) carries this tag, as in the AI studio system. */\nexport const HIDDEN_TAG = \"hidden\";\n\nexport interface AppItem {\n key: string;\n kind: Kind;\n /** The platform template the feature runs; absent: \"describe it yourself\" (a plain feature of the kind). */\n template?: AIPublicTemplate;\n trend: boolean;\n /** Ways to pay; empty and not `unpayable`: free for the player. */\n options: Array<{ id: string; price: ResourceLine[] }>;\n /** The feature has a price, but none of its ways to pay works here (Aspis inside the iOS app): no button. */\n unpayable: boolean;\n /** Free generations left today; null: the feature gives none. */\n freeLeft: number | null;\n order: number;\n maxChars: number;\n}\n\n/**\n * The cards of the app, in the order of the tabs, then the template's order. A feature whose template was taken off\n * the site (a `PlatformTemplateId` without its card) is not shown: running it would only answer \"unavailable\".\n */\nexport function appItems(\n section: AIPublicDefinitions | null | undefined,\n /** false: Aspis cannot be paid here (the iOS app, `canPayWithPlatformCredits`), its prices are dropped. */\n creditsPayable = true,\n): AppItem[] {\n if (!section || section.Enabled === false) return [];\n const items: AppItem[] = [];\n for (const [key, f] of Object.entries(section.Features ?? {})) {\n if (!f || (f.Tags ?? []).includes(HIDDEN_TAG)) continue;\n const kind = f.Modality as Kind;\n if (!KINDS.includes(kind)) continue;\n if (f.PlatformTemplateId && !f.PlatformTemplate) continue;\n const template = f.PlatformTemplate ?? undefined;\n const all = optionsOf(f);\n const options = creditsPayable\n ? all\n : all.filter((o) => !o.price.some((l) => l.kind === \"credits\"));\n items.push({\n key,\n kind,\n ...(template ? { template } : {}),\n trend: template?.IsTrend === true,\n options,\n unpayable: all.length > 0 && options.length === 0,\n freeLeft:\n f.FreePerDay && f.FreePerDay > 0\n ? Math.max(0, f.FreeLeftToday ?? 0)\n : null,\n order: template?.Order ?? Number.MAX_SAFE_INTEGER,\n maxChars: Math.max(0, Number(f.MaxPromptChars ?? 0)),\n });\n }\n return items.sort(\n (a, b) =>\n KINDS.indexOf(a.kind) - KINDS.indexOf(b.kind) ||\n Number(!!a.template) - Number(!!b.template) ||\n a.order - b.order ||\n a.key.localeCompare(b.key),\n );\n}\n\nfunction optionsOf(f: AIPublicFeature): AppItem[\"options\"] {\n return Object.entries(f.PriceOptions ?? {})\n .filter(([, o]) => o)\n .map(([oid, o]) => ({\n id: o?.OptionID || oid,\n price: costLines(o?.Cost as never).filter((l) => l.kind !== \"other\"),\n }))\n .filter((o) => o.price.length > 0);\n}\n\n/** Kinds that have at least one card, in tab order. One kind: the app opens straight on it, without tabs. */\nexport function kindsOf(items: readonly AppItem[]): Kind[] {\n return KINDS.filter((k) => items.some((i) => i.kind === k));\n}\n\n/**\n * A text of the template in the player's language: the interface language, then the device's, then English, then\n * any. Templates of the platform carry nine languages; the UI kit speaks English and Russian.\n */\nexport function localized(\n text: Record<string, string> | null | undefined,\n language: string,\n deviceLanguage: string = typeof navigator === \"undefined\"\n ? \"en\"\n : navigator.language,\n): string {\n if (!text) return \"\";\n const device = deviceLanguage.slice(0, 2).toLowerCase();\n return (\n text[language] ||\n text[device] ||\n text.en ||\n Object.values(text).find((v) => !!v) ||\n \"\"\n );\n}\n\n/** Required photo slots the player has not filled yet (their keys). */\nexport function missingInputs(\n template: AIPublicTemplate | undefined,\n files: Readonly<Record<string, AIInputImage | undefined>>,\n): string[] {\n return (template?.Inputs ?? [])\n .filter((i) => i.Required && i.Key && !files[i.Key])\n .map((i) => i.Key!);\n}\n\n/** Required variables left empty (no value and no default). */\nexport function missingValues(\n template: AIPublicTemplate | undefined,\n values: Readonly<Record<string, string | undefined>>,\n): string[] {\n return (template?.Variables ?? [])\n .filter(\n (v) =>\n v.Required && v.Name && !(values[v.Name]?.trim() || v.Default?.trim()),\n )\n .map((v) => v.Name!);\n}\n\n/** What the button costs right now: free (with how many are left), or the first option the player can pay. */\nexport type PriceLabel =\n | { free: true; left: number }\n | { free: false; option?: AppItem[\"options\"][number] };\n\nexport function priceLabelOf(\n item: AppItem,\n affordable: (price: ResourceLine[]) => boolean,\n): PriceLabel {\n if (item.freeLeft !== null && item.freeLeft > 0)\n return { free: true, left: item.freeLeft };\n if (item.options.length === 0)\n return item.unpayable\n ? { free: false }\n : { free: true, left: Number.POSITIVE_INFINITY };\n const option =\n item.options.find((o) => affordable(o.price)) ?? item.options[0];\n return option ? { free: false, option } : { free: false };\n}\n\n/** The longest side of a photo sent to the server: enough for every model, small enough for a phone upload. */\nexport const MAX_PHOTO_SIDE = 1536;\n\n/** The size a photo is scaled to (never up). */\nexport function fitSize(\n width: number,\n height: number,\n max = MAX_PHOTO_SIDE,\n): { width: number; height: number } {\n const longest = Math.max(width, height);\n if (longest <= max || longest <= 0) return { width, height };\n const k = max / longest;\n return { width: Math.round(width * k), height: Math.round(height * k) };\n}\n\n/** What a finished generation shows. */\nexport type ResultKind = \"image\" | \"audio\" | \"video\" | \"file\";\nexport function resultKind(contentType: string | null | undefined): ResultKind {\n const ct = (contentType ?? \"\").toLowerCase();\n if (ct.startsWith(\"image/\")) return \"image\";\n if (ct.startsWith(\"audio/\")) return \"audio\";\n if (ct.startsWith(\"video/\")) return \"video\";\n return \"file\";\n}\n"
62
+ "content": "// Pure helpers of the AI generator app: covered by model.test.ts. The app is the title's AI section: the server\n// sends the features sanitized (no prompts, no models), each with its price, its free generations and, for a\n// platform template, the template's card.\n\nimport type {\n AIPublicDefinitions,\n AIPublicFeature,\n AIPublicSetting,\n AIPublicTemplate,\n AIInputImage,\n AITemplatePrice,\n} from \"@idosgames/core\";\nimport { costLines, type ResourceLine } from \"@idosgames/react/ui\";\n\n/** What the app makes. Text features are for the game's own code, not for this app. */\nexport type Kind = \"Video\" | \"Image\" | \"Audio\" | \"Music\" | \"ThreeD\";\n\n/** Tab order: video first (trends live there), then pictures, voice, music, 3D. */\nexport const KINDS: readonly Kind[] = [\n \"Video\",\n \"Image\",\n \"Audio\",\n \"Music\",\n \"ThreeD\",\n];\n\n/** A feature kept for the game's own code (NPC lines and the like) carries this tag, as in the AI studio system. */\nexport const HIDDEN_TAG = \"hidden\";\n\nexport interface AppItem {\n key: string;\n kind: Kind;\n /** The platform template the feature runs; absent: \"describe it yourself\" (a plain feature of the kind). */\n template?: AIPublicTemplate;\n trend: boolean;\n /** Ways to pay; empty and not `unpayable`: free for the player. */\n options: Array<{ id: string; price: ResourceLine[] }>;\n /** The feature has a price, but none of its ways to pay works here (Aspis inside the iOS app): no button. */\n unpayable: boolean;\n /** Free generations left today; null: the feature gives none. */\n freeLeft: number | null;\n order: number;\n maxChars: number;\n}\n\n/**\n * The cards of the app, in the order of the tabs, then the template's order. A feature whose template was taken off\n * the site (a `PlatformTemplateId` without its card) is not shown: running it would only answer \"unavailable\".\n */\nexport function appItems(\n section: AIPublicDefinitions | null | undefined,\n /** false: Aspis cannot be paid here (the iOS app, `canPayWithPlatformCredits`), its prices are dropped. */\n creditsPayable = true,\n): AppItem[] {\n if (!section || section.Enabled === false) return [];\n const items: AppItem[] = [];\n for (const [key, f] of Object.entries(section.Features ?? {})) {\n if (!f || (f.Tags ?? []).includes(HIDDEN_TAG)) continue;\n const kind = f.Modality as Kind;\n if (!KINDS.includes(kind)) continue;\n if (f.PlatformTemplateId && !f.PlatformTemplate) continue;\n const template = f.PlatformTemplate ?? undefined;\n const all = optionsOf(f);\n const options = creditsPayable\n ? all\n : all.filter((o) => !o.price.some((l) => l.kind === \"credits\"));\n items.push({\n key,\n kind,\n ...(template ? { template } : {}),\n trend: template?.IsTrend === true,\n options,\n unpayable: all.length > 0 && options.length === 0,\n freeLeft:\n f.FreePerDay && f.FreePerDay > 0\n ? Math.max(0, f.FreeLeftToday ?? 0)\n : null,\n order: template?.Order ?? Number.MAX_SAFE_INTEGER,\n maxChars: Math.max(0, Number(f.MaxPromptChars ?? 0)),\n });\n }\n return items.sort(\n (a, b) =>\n KINDS.indexOf(a.kind) - KINDS.indexOf(b.kind) ||\n Number(!!a.template) - Number(!!b.template) ||\n a.order - b.order ||\n a.key.localeCompare(b.key),\n );\n}\n\nfunction optionsOf(f: AIPublicFeature): AppItem[\"options\"] {\n return Object.entries(f.PriceOptions ?? {})\n .filter(([, o]) => o)\n .map(([oid, o]) => ({\n id: o?.OptionID || oid,\n price: costLines(o?.Cost as never).filter((l) => l.kind !== \"other\"),\n }))\n .filter((o) => o.price.length > 0);\n}\n\n/** Kinds that have at least one card, in tab order. One kind: the app opens straight on it, without tabs. */\nexport function kindsOf(items: readonly AppItem[]): Kind[] {\n return KINDS.filter((k) => items.some((i) => i.kind === k));\n}\n\n/**\n * A text of the template in the player's language: the interface language, then the device's, then English, then\n * any. Templates of the platform carry nine languages; the UI kit speaks English and Russian.\n */\nexport function localized(\n text: Record<string, string> | null | undefined,\n language: string,\n deviceLanguage: string = typeof navigator === \"undefined\"\n ? \"en\"\n : navigator.language,\n): string {\n if (!text) return \"\";\n const device = deviceLanguage.slice(0, 2).toLowerCase();\n return (\n text[language] ||\n text[device] ||\n text.en ||\n Object.values(text).find((v) => !!v) ||\n \"\"\n );\n}\n\n/** Required photo slots the player has not filled yet (their keys). */\nexport function missingInputs(\n template: AIPublicTemplate | undefined,\n files: Readonly<Record<string, AIInputImage | undefined>>,\n): string[] {\n return (template?.Inputs ?? [])\n .filter((i) => i.Required && i.Key && !files[i.Key])\n .map((i) => i.Key!);\n}\n\n/** Required variables left empty (no value and no default). */\nexport function missingValues(\n template: AIPublicTemplate | undefined,\n values: Readonly<Record<string, string | undefined>>,\n): string[] {\n return (template?.Variables ?? [])\n .filter(\n (v) =>\n v.Required && v.Name && !(values[v.Name]?.trim() || v.Default?.trim()),\n )\n .map((v) => v.Name!);\n}\n\n// ── Settings the author gave to the player (texture, polygons, resolution, length…) ───────────\n\n/**\n * The value a setting sends: a number is kept inside the author's range (a typed 999999 polygons becomes the maximum),\n * anything else as it is. Empty: the author's value.\n */\nexport function settingValue(s: AIPublicSetting, raw: string | undefined): string {\n const v = (raw ?? \"\").trim();\n if (!v) return s.Default ?? \"\";\n if (s.Kind !== \"Number\") return v;\n let n = Number(v);\n if (!Number.isFinite(n)) return s.Default ?? \"\";\n if (s.Min != null && n < s.Min) n = s.Min;\n if (s.Max != null && n > s.Max) n = s.Max;\n return String(n);\n}\n\n/**\n * The settings the player changed (key → value). Only those go to the server: the author's own values price the card\n * as it is, and an empty result means \"the author's settings, the card's price\".\n */\nexport function changedSettings(\n template: AIPublicTemplate | undefined,\n raw: Readonly<Record<string, string | undefined>>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const s of template?.Settings ?? []) {\n if (!s.Key || raw[s.Key] === undefined) continue;\n const v = settingValue(s, raw[s.Key]);\n if (v && v !== (s.Default ?? \"\")) out[s.Key] = v;\n }\n return out;\n}\n\n/**\n * The card priced for the player's settings (`client.ai.estimateTemplatePrice`): the same ways to pay, each with the\n * server's price for this choice. A way the server left out (a store purchase cannot pay for a pricier choice) is gone.\n */\nexport function repriced(item: AppItem, price: AITemplatePrice): AppItem {\n const fresh = optionsOf({ PriceOptions: price.PriceOptions } as AIPublicFeature);\n const options = item.options\n .map((o) => fresh.find((f) => f.id === o.id))\n .filter((o): o is AppItem[\"options\"][number] => !!o);\n // No way left to pay for this choice: not free, the button just has no price (unpayable).\n return { ...item, options, unpayable: item.unpayable || (item.options.length > 0 && options.length === 0) };\n}\n\n/** What the button costs right now: free (with how many are left), or the first option the player can pay. */\nexport type PriceLabel =\n | { free: true; left: number }\n | { free: false; option?: AppItem[\"options\"][number] };\n\nexport function priceLabelOf(\n item: AppItem,\n affordable: (price: ResourceLine[]) => boolean,\n): PriceLabel {\n if (item.freeLeft !== null && item.freeLeft > 0)\n return { free: true, left: item.freeLeft };\n if (item.options.length === 0)\n return item.unpayable\n ? { free: false }\n : { free: true, left: Number.POSITIVE_INFINITY };\n const option =\n item.options.find((o) => affordable(o.price)) ?? item.options[0];\n return option ? { free: false, option } : { free: false };\n}\n\n/** The longest side of a photo sent to the server: enough for every model, small enough for a phone upload. */\nexport const MAX_PHOTO_SIDE = 1536;\n\n/** The size a photo is scaled to (never up). */\nexport function fitSize(\n width: number,\n height: number,\n max = MAX_PHOTO_SIDE,\n): { width: number; height: number } {\n const longest = Math.max(width, height);\n if (longest <= max || longest <= 0) return { width, height };\n const k = max / longest;\n return { width: Math.round(width * k), height: Math.round(height * k) };\n}\n\n/** What a finished generation shows. */\nexport type ResultKind = \"image\" | \"audio\" | \"video\" | \"file\";\nexport function resultKind(contentType: string | null | undefined): ResultKind {\n const ct = (contentType ?? \"\").toLowerCase();\n if (ct.startsWith(\"image/\")) return \"image\";\n if (ct.startsWith(\"audio/\")) return \"audio\";\n if (ct.startsWith(\"video/\")) return \"video\";\n return \"file\";\n}\n"
63
63
  },
64
64
  {
65
65
  "path": "module.ts",
@@ -30,7 +30,7 @@
30
30
  "version": "0.1.3"
31
31
  },
32
32
  "dependencies": {
33
- "@idosgames/core": "0.26.2",
33
+ "@idosgames/core": "0.26.3",
34
34
  "@idosgames/module-sdk": "0.13.4",
35
35
  "@idosgames/react": "0.7.2",
36
36
  "react": "19.2.7"
@@ -45,7 +45,7 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@idosgames/core": "0.26.2",
48
+ "@idosgames/core": "0.26.3",
49
49
  "@idosgames/module-sdk": "0.13.4",
50
50
  "@idosgames/react": "0.7.2",
51
51
  "@idosgames/wallet": "0.6.10",
@@ -31,7 +31,7 @@
31
31
  "version": "0.1.1"
32
32
  },
33
33
  "dependencies": {
34
- "@idosgames/core": "0.26.2",
34
+ "@idosgames/core": "0.26.3",
35
35
  "@idosgames/module-sdk": "0.13.4",
36
36
  "@idosgames/react": "0.7.2",
37
37
  "react": "19.2.7"
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@idosgames/core": "0.26.2",
57
+ "@idosgames/core": "0.26.3",
58
58
  "@idosgames/module-sdk": "0.13.4",
59
59
  "@idosgames/react": "0.7.2",
60
60
  "react": "19.2.7"
@@ -35,7 +35,7 @@
35
35
  "version": "0.1.2"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.2",
38
+ "@idosgames/core": "0.26.3",
39
39
  "@idosgames/module-sdk": "0.13.4",
40
40
  "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
@@ -34,7 +34,7 @@
34
34
  "version": "0.1.3"
35
35
  },
36
36
  "dependencies": {
37
- "@idosgames/core": "0.26.2",
37
+ "@idosgames/core": "0.26.3",
38
38
  "@idosgames/module-sdk": "0.13.4",
39
39
  "@idosgames/react": "0.7.2",
40
40
  "react": "19.2.7"
@@ -32,7 +32,7 @@
32
32
  "version": "0.1.3"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.26.2",
35
+ "@idosgames/core": "0.26.3",
36
36
  "@idosgames/module-sdk": "0.13.4",
37
37
  "@idosgames/react": "0.7.2",
38
38
  "react": "19.2.7"
@@ -35,7 +35,7 @@
35
35
  "version": "0.1.2"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.2",
38
+ "@idosgames/core": "0.26.3",
39
39
  "@idosgames/module-sdk": "0.13.4",
40
40
  "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
@@ -34,7 +34,7 @@
34
34
  "version": "0.1.3"
35
35
  },
36
36
  "dependencies": {
37
- "@idosgames/core": "0.26.2",
37
+ "@idosgames/core": "0.26.3",
38
38
  "@idosgames/module-sdk": "0.13.4",
39
39
  "@idosgames/react": "0.7.2",
40
40
  "react": "19.2.7"
@@ -37,7 +37,7 @@
37
37
  "version": "0.1.5"
38
38
  },
39
39
  "dependencies": {
40
- "@idosgames/core": "0.26.2",
40
+ "@idosgames/core": "0.26.3",
41
41
  "@idosgames/module-sdk": "0.13.4",
42
42
  "@idosgames/react": "0.7.2",
43
43
  "react": "19.2.7"
@@ -35,7 +35,7 @@
35
35
  "version": "0.1.3"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.2",
38
+ "@idosgames/core": "0.26.3",
39
39
  "@idosgames/module-sdk": "0.13.4",
40
40
  "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@idosgames/characters": "0.1.3",
73
- "@idosgames/core": "0.26.2",
73
+ "@idosgames/core": "0.26.3",
74
74
  "@idosgames/module-sdk": "0.13.4",
75
75
  "@idosgames/react": "0.7.2",
76
76
  "phaser": "4.2.1",
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "@idosgames/characters": "0.1.3",
79
- "@idosgames/core": "0.26.2",
79
+ "@idosgames/core": "0.26.3",
80
80
  "@idosgames/module-sdk": "0.13.4",
81
81
  "@idosgames/react": "0.7.2",
82
82
  "phaser": "4.2.1",
@@ -32,7 +32,7 @@
32
32
  "version": "0.1.1"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.26.2",
35
+ "@idosgames/core": "0.26.3",
36
36
  "@idosgames/module-sdk": "0.13.4",
37
37
  "@idosgames/react": "0.7.2",
38
38
  "react": "19.2.7"
@@ -35,7 +35,7 @@
35
35
  "version": "0.1.3"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.2",
38
+ "@idosgames/core": "0.26.3",
39
39
  "@idosgames/module-sdk": "0.13.4",
40
40
  "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@idosgames/characters": "0.1.3",
43
- "@idosgames/core": "0.26.2",
43
+ "@idosgames/core": "0.26.3",
44
44
  "@idosgames/game-kit": "0.1.1",
45
45
  "@idosgames/module-sdk": "0.13.4",
46
46
  "@idosgames/react": "0.7.2",
@@ -36,7 +36,7 @@
36
36
  "version": "0.1.5"
37
37
  },
38
38
  "dependencies": {
39
- "@idosgames/core": "0.26.2",
39
+ "@idosgames/core": "0.26.3",
40
40
  "@idosgames/module-sdk": "0.13.4",
41
41
  "@idosgames/react": "0.7.2",
42
42
  "react": "19.2.7"
@@ -32,7 +32,7 @@
32
32
  "version": "0.1.4"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.26.2",
35
+ "@idosgames/core": "0.26.3",
36
36
  "@idosgames/module-sdk": "0.13.4",
37
37
  "@idosgames/react": "0.7.2",
38
38
  "react": "19.2.7"
@@ -36,7 +36,7 @@
36
36
  "version": "0.1.3"
37
37
  },
38
38
  "dependencies": {
39
- "@idosgames/core": "0.26.2",
39
+ "@idosgames/core": "0.26.3",
40
40
  "@idosgames/module-sdk": "0.13.4",
41
41
  "@idosgames/react": "0.7.2",
42
42
  "react": "19.2.7"
@@ -36,7 +36,7 @@
36
36
  "version": "0.1.3"
37
37
  },
38
38
  "dependencies": {
39
- "@idosgames/core": "0.26.2",
39
+ "@idosgames/core": "0.26.3",
40
40
  "@idosgames/module-sdk": "0.13.4",
41
41
  "@idosgames/react": "0.7.2",
42
42
  "react": "19.2.7"
@@ -39,7 +39,7 @@
39
39
  "version": "0.2.1"
40
40
  },
41
41
  "dependencies": {
42
- "@idosgames/core": "0.26.2",
42
+ "@idosgames/core": "0.26.3",
43
43
  "@idosgames/module-sdk": "0.13.4",
44
44
  "@idosgames/react": "0.7.2",
45
45
  "react": "19.2.7"
@@ -32,7 +32,7 @@
32
32
  "version": "0.1.3"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.26.2",
35
+ "@idosgames/core": "0.26.3",
36
36
  "@idosgames/module-sdk": "0.13.4",
37
37
  "@idosgames/react": "0.7.2",
38
38
  "react": "19.2.7"
@@ -34,7 +34,7 @@
34
34
  "version": "0.1.5"
35
35
  },
36
36
  "dependencies": {
37
- "@idosgames/core": "0.26.2",
37
+ "@idosgames/core": "0.26.3",
38
38
  "@idosgames/module-sdk": "0.13.4",
39
39
  "@idosgames/react": "0.7.2",
40
40
  "react": "19.2.7"
@@ -33,7 +33,7 @@
33
33
  "version": "0.1.2"
34
34
  },
35
35
  "dependencies": {
36
- "@idosgames/core": "0.26.2",
36
+ "@idosgames/core": "0.26.3",
37
37
  "@idosgames/module-sdk": "0.13.4",
38
38
  "@idosgames/react": "0.7.2",
39
39
  "react": "19.2.7"
@@ -34,7 +34,7 @@
34
34
  "version": "0.1.3"
35
35
  },
36
36
  "dependencies": {
37
- "@idosgames/core": "0.26.2",
37
+ "@idosgames/core": "0.26.3",
38
38
  "@idosgames/module-sdk": "0.13.4",
39
39
  "@idosgames/react": "0.7.2",
40
40
  "react": "19.2.7"
@@ -31,7 +31,7 @@
31
31
  "version": "0.1.3"
32
32
  },
33
33
  "dependencies": {
34
- "@idosgames/core": "0.26.2",
34
+ "@idosgames/core": "0.26.3",
35
35
  "@idosgames/module-sdk": "0.13.4",
36
36
  "@idosgames/react": "0.7.2",
37
37
  "react": "19.2.7"
@@ -36,7 +36,7 @@
36
36
  "version": "0.1.3"
37
37
  },
38
38
  "dependencies": {
39
- "@idosgames/core": "0.26.2",
39
+ "@idosgames/core": "0.26.3",
40
40
  "@idosgames/module-sdk": "0.13.4",
41
41
  "@idosgames/react": "0.7.2",
42
42
  "react": "19.2.7"
@@ -36,7 +36,7 @@
36
36
  "version": "0.1.4"
37
37
  },
38
38
  "dependencies": {
39
- "@idosgames/core": "0.26.2",
39
+ "@idosgames/core": "0.26.3",
40
40
  "@idosgames/module-sdk": "0.13.4",
41
41
  "@idosgames/react": "0.7.2",
42
42
  "react": "19.2.7"
@@ -32,7 +32,7 @@
32
32
  "version": "0.1.4"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.26.2",
35
+ "@idosgames/core": "0.26.3",
36
36
  "@idosgames/module-sdk": "0.13.4",
37
37
  "@idosgames/react": "0.7.2",
38
38
  "react": "19.2.7"
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@idosgames/characters": "0.1.3",
58
- "@idosgames/core": "0.26.2",
58
+ "@idosgames/core": "0.26.3",
59
59
  "@idosgames/module-sdk": "0.13.4",
60
60
  "@idosgames/react": "0.7.2",
61
61
  "@idosgames/timeline": "0.1.0",
@@ -85,7 +85,7 @@
85
85
  },
86
86
  "dependencies": {
87
87
  "@idosgames/characters": "0.1.3",
88
- "@idosgames/core": "0.26.2",
88
+ "@idosgames/core": "0.26.3",
89
89
  "@idosgames/module-sdk": "0.13.4",
90
90
  "@idosgames/react": "0.7.2",
91
91
  "react": "19.2.7",
@@ -39,7 +39,7 @@
39
39
  "version": "0.1.3"
40
40
  },
41
41
  "dependencies": {
42
- "@idosgames/core": "0.26.2",
42
+ "@idosgames/core": "0.26.3",
43
43
  "@idosgames/module-sdk": "0.13.4",
44
44
  "@idosgames/react": "0.7.2",
45
45
  "react": "19.2.7"