polats commited on
Commit
c9dd954
·
verified ·
1 Parent(s): 7ef6a4e

Deploy Karate Wiener (kimodo kata maker)

Browse files
Files changed (5) hide show
  1. app.py +51 -0
  2. comic_shots.py +79 -0
  3. tabs/dojo.drawer.html +63 -63
  4. tabs/dojo.tab.css +138 -4
  5. tabs/dojo.tab.js +292 -139
app.py CHANGED
@@ -133,6 +133,8 @@ _DEFAULT_SCENE_PATH = Path(__file__).parent / "assets" / "default_scene.webp"
133
  DIT360_SPACE = os.environ.get("KIMODO_DIT360_SPACE", "Insta360-Research/DiT360").strip()
134
  KLEIN_SPACE = os.environ.get("KIMODO_KLEIN_SPACE", "polats/tiny-army-klein-zerogpu").strip()
135
  TRIPOSPLAT_SPACE = os.environ.get("KIMODO_TRIPOSPLAT_SPACE", "polats/kimodo-triposplat-zerogpu").strip()
 
 
136
 
137
  # --- Pluggable side tabs ---------------------------------------------------
138
  # A NEW side tab is added by DROPPING FILES into ./tabs — NO app.py edit needed,
@@ -3571,6 +3573,45 @@ def generate_dojo_scene(payload_json: str | dict | None) -> str:
3571
  return
3572
 
3573
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3574
  def _copy_creator_fields(dst: dict, src: dict | None) -> dict:
3575
  if not src:
3576
  return dst
@@ -6466,6 +6507,10 @@ with gr.Blocks(title="Kimodo ZeroGPU", css=_BLOCKS_CSS) as demo:
6466
  dojo_payload = gr.Textbox(value="", label="Dojo payload", elem_id="dojo-payload", container=False)
6467
  dojo_result = gr.Textbox(value="", label="Dojo result", elem_id="dojo-result", container=False)
6468
  dojo_btn = gr.Button("Generate dojo scene", elem_id="dojo-btn")
 
 
 
 
6469
  # Viewer is the only visible component — it fills the screen. Everything else
6470
  # is wrapped in .kimodo-chrome and hidden (kept in the DOM so the drawers can
6471
  # still read its inputs / fire its hidden buttons).
@@ -6608,6 +6653,12 @@ with gr.Blocks(title="Kimodo ZeroGPU", css=_BLOCKS_CSS) as demo:
6608
  outputs=[dojo_result],
6609
  api_name=False,
6610
  )
 
 
 
 
 
 
6611
 
6612
  # Display model drives the persistent viewer live via postMessage (no reload).
6613
  # Clothing is handled by the fixed browser drawer so it can behave like kata's drawers.
 
133
  DIT360_SPACE = os.environ.get("KIMODO_DIT360_SPACE", "Insta360-Research/DiT360").strip()
134
  KLEIN_SPACE = os.environ.get("KIMODO_KLEIN_SPACE", "polats/tiny-army-klein-zerogpu").strip()
135
  TRIPOSPLAT_SPACE = os.environ.get("KIMODO_TRIPOSPLAT_SPACE", "polats/kimodo-triposplat-zerogpu").strip()
136
+ # tiny-aya (Cohere Tiny Aya) text model — used to suggest/improve dojo scene prompts.
137
+ TINY_AYA_SPACE = os.environ.get("KIMODO_TINY_AYA_SPACE", "polats/tiny-army-tiny-aya-zerogpu").strip()
138
 
139
  # --- Pluggable side tabs ---------------------------------------------------
140
  # A NEW side tab is added by DROPPING FILES into ./tabs — NO app.py edit needed,
 
3573
  return
3574
 
3575
 
3576
+ def improve_dojo_prompt(payload_json: str | dict | None) -> str:
3577
+ """Use tiny-aya to turn a few keywords (or nothing) into a full dojo scene prompt.
3578
+ Returns JSON: {"ok": true, "prompt": "..."} or {"ok": false, "error": "..."}."""
3579
+ raw = payload_json
3580
+ if not isinstance(raw, dict):
3581
+ try:
3582
+ raw = json.loads(raw or "{}")
3583
+ except Exception:
3584
+ raw = {"words": str(payload_json or "")}
3585
+ words = (raw.get("words") or raw.get("prompt") or "").strip()
3586
+ system = (
3587
+ "You write a single vivid text-to-image prompt for a 360-degree room used as a "
3588
+ "martial-arts dojo backdrop. Reply with ONLY the prompt on one line: no preamble, "
3589
+ "no quotes, under 55 words. Describe a full room interior (floor, walls, lighting) "
3590
+ "and end with: full room visible, high detail, no people, no text."
3591
+ )
3592
+ user = (f"Turn these ideas into a dojo scene prompt: {words}" if words
3593
+ else "Invent a creative, unexpected dojo scene prompt.")
3594
+ if not TINY_AYA_SPACE:
3595
+ return json.dumps({"ok": False, "error": "tiny-aya space not configured"})
3596
+ try:
3597
+ from gradio_client import Client
3598
+ client = Client(TINY_AYA_SPACE, token=_hf_token())
3599
+ out = client.predict(system, user, 220, 0.9, api_name="/generate")
3600
+ text = (out or "").strip()
3601
+ # Prefer the first substantial line (drops any stray preamble the model adds).
3602
+ best = ""
3603
+ for line in text.splitlines():
3604
+ cand = line.strip().strip('"').strip("'").lstrip("-*•0123456789. ").strip()
3605
+ if len(cand) > len(best):
3606
+ best = cand
3607
+ text = " ".join((best or text).split())[:500]
3608
+ if not text:
3609
+ return json.dumps({"ok": False, "error": "empty suggestion"})
3610
+ return json.dumps({"ok": True, "prompt": text})
3611
+ except Exception as exc:
3612
+ return json.dumps({"ok": False, "error": str(exc)[:180]})
3613
+
3614
+
3615
  def _copy_creator_fields(dst: dict, src: dict | None) -> dict:
3616
  if not src:
3617
  return dst
 
6507
  dojo_payload = gr.Textbox(value="", label="Dojo payload", elem_id="dojo-payload", container=False)
6508
  dojo_result = gr.Textbox(value="", label="Dojo result", elem_id="dojo-result", container=False)
6509
  dojo_btn = gr.Button("Generate dojo scene", elem_id="dojo-btn")
6510
+ # tiny-aya prompt helper: JSON {"words": "..."} -> JSON {"ok", "prompt"}.
6511
+ dojo_suggest_payload = gr.Textbox(value="", label="Dojo suggest payload", elem_id="dojo-suggest-payload", container=False)
6512
+ dojo_suggest_result = gr.Textbox(value="", label="Dojo suggest result", elem_id="dojo-suggest-result", container=False)
6513
+ dojo_suggest_btn = gr.Button("Suggest dojo prompt", elem_id="dojo-suggest-btn")
6514
  # Viewer is the only visible component — it fills the screen. Everything else
6515
  # is wrapped in .kimodo-chrome and hidden (kept in the DOM so the drawers can
6516
  # still read its inputs / fire its hidden buttons).
 
6653
  outputs=[dojo_result],
6654
  api_name=False,
6655
  )
6656
+ dojo_suggest_btn.click(
6657
+ fn=improve_dojo_prompt,
6658
+ inputs=[dojo_suggest_payload],
6659
+ outputs=[dojo_suggest_result],
6660
+ api_name=False,
6661
+ )
6662
 
6663
  # Display model drives the persistent viewer live via postMessage (no reload).
6664
  # Clothing is handled by the fixed browser drawer so it can behave like kata's drawers.
comic_shots.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shot grammar for the 4-koma stance comics: named shots -> deterministic camera,
2
+ computed purely from the skeleton frame (bbox + hip heading). The LLM never sees any
3
+ of this; it only ever picks captions. Cameras are plain pinhole (eye/target/fov/roll)
4
+ so the same parameters port 1:1 to the three.js viewer later."""
5
+ import numpy as np
6
+
7
+ PARENTS = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19]
8
+ PEL, LHIP, RHIP, HEAD = 0, 1, 2, 15
9
+ LEFTJ = {1, 4, 7, 10, 13, 16, 18, 20}
10
+ RIGHTJ = {2, 5, 8, 11, 14, 17, 19, 21}
11
+
12
+ # az = horizontal angle of the camera relative to the character's facing direction
13
+ # (0 = head-on). el = elevation in degrees (negative = camera low, looking up).
14
+ # margin = how much air around the joint bbox. roll = dutch tilt.
15
+ SHOTS = {
16
+ "establishing": dict(az=38.0, el=10.0, margin=1.55, roll=0.0, fov=40.0),
17
+ "action": dict(az=55.0, el=4.0, margin=1.22, roll=-4.0, fov=46.0),
18
+ "gag": dict(az=22.0, el=16.0, margin=1.12, roll=13.0, fov=50.0),
19
+ "hero": dict(az=32.0, el=-16.0, margin=1.42, roll=0.0, fov=38.0),
20
+ }
21
+
22
+
23
+ def heading(Pf):
24
+ """Facing angle about Y from the hip line (0 = facing +Z), as in _build_stances."""
25
+ d = Pf[RHIP] - Pf[LHIP]
26
+ return float(np.arctan2(d[2], -d[0]))
27
+
28
+
29
+ def make_camera(shot_name, Pf):
30
+ """Camera dict for one skeleton frame [22,3]. Frames the joint bbox with the
31
+ shot's margin, oriented relative to the character's heading."""
32
+ s = SHOTS[shot_name]
33
+ lo, hi = Pf.min(axis=0), Pf.max(axis=0)
34
+ target = (lo + hi) / 2.0
35
+ radius = float(np.linalg.norm(Pf - target, axis=1).max())
36
+ fov = np.radians(s["fov"])
37
+ dist = s["margin"] * radius / np.tan(fov / 2.0)
38
+ theta = heading(Pf) + np.radians(s["az"])
39
+ el = np.radians(s["el"])
40
+ direction = np.array([np.sin(theta) * np.cos(el), np.sin(el), np.cos(theta) * np.cos(el)])
41
+ eye = target + direction * dist
42
+ # Don't let the camera dip below the floor (hero shots on grounded poses).
43
+ eye[1] = max(eye[1], 0.12)
44
+ return dict(eye=eye, target=target, fov=fov, roll=np.radians(s["roll"]))
45
+
46
+
47
+ def project(points, cam, W, H):
48
+ """Pinhole-project [N,3] world points -> ([N,2] pixel coords, [N] view depth)."""
49
+ pts = np.asarray(points, np.float64).reshape(-1, 3)
50
+ eye, target = cam["eye"], cam["target"]
51
+ fwd = target - eye
52
+ fwd = fwd / np.linalg.norm(fwd)
53
+ right = np.cross(fwd, np.array([0.0, 1.0, 0.0]))
54
+ right = right / (np.linalg.norm(right) + 1e-9)
55
+ up = np.cross(right, fwd)
56
+ r = cam["roll"]
57
+ if abs(r) > 1e-6: # dutch tilt: rotate the basis about the view axis
58
+ c, s = np.cos(r), np.sin(r)
59
+ right, up = c * right + s * up, -s * right + c * up
60
+ rel = pts - eye
61
+ x = rel @ right
62
+ y = rel @ up
63
+ z = rel @ fwd
64
+ z = np.maximum(z, 1e-4)
65
+ f = (H / 2.0) / np.tan(cam["fov"] / 2.0)
66
+ px = W / 2.0 + f * x / z
67
+ py = H / 2.0 - f * y / z
68
+ return np.stack([px, py], axis=1), z
69
+
70
+
71
+ def ground_grid(center, span=2.6, step=0.65):
72
+ """3D segments of a floor grid (y=0) around the character, for scene depth."""
73
+ segs = []
74
+ cx, cz = float(center[0]), float(center[2])
75
+ ticks = np.arange(-span, span + 1e-6, step)
76
+ for t in ticks:
77
+ segs.append(((cx - span, 0.0, cz + t), (cx + span, 0.0, cz + t)))
78
+ segs.append(((cx + t, 0.0, cz - span), (cx + t, 0.0, cz + span)))
79
+ return segs
tabs/dojo.drawer.html CHANGED
@@ -1,73 +1,73 @@
1
- <aside id="kimodo-dojo-drawer" class="kimodo-drawer" aria-label="Dojo scene drawer">
2
  <h2>Dojo</h2>
3
- <div class="kimodo-drawer-sub">Create a room scene for the viewer.</div>
4
 
5
- <div class="kimodo-cz-seg kimodo-dojo-mode" id="kimodo-dojo-mode" aria-label="Dojo scene type">
6
- <button type="button" class="kimodo-cz-segbtn active" data-mode="splat">Splat</button>
7
- <button type="button" class="kimodo-cz-segbtn" data-mode="panorama">Panorama</button>
 
 
 
 
 
 
8
  </div>
9
 
10
- <div class="kimodo-dojo-settings" aria-label="Dojo room settings">
11
- <label class="kimodo-dojo-check">
12
- <input id="kimodo-dojo-skybox" type="checkbox" />
13
- <span>Use panorama as skybox in splat</span>
14
- </label>
15
- <label class="kimodo-dojo-range" for="kimodo-dojo-room-size">
16
- <span>Room size <output id="kimodo-dojo-room-size-value">8.0</output>m</span>
17
- <input id="kimodo-dojo-room-size" type="range" min="3" max="14" value="8" step="0.5" />
18
- </label>
19
- <label class="kimodo-dojo-range" for="kimodo-dojo-room-height">
20
- <span>Room height <output id="kimodo-dojo-room-height-value">3.2</output>m</span>
21
- <input id="kimodo-dojo-room-height" type="range" min="2" max="6" value="3.2" step="0.2" />
22
- </label>
23
- </div>
24
-
25
- <label class="kimodo-dojo-label" for="kimodo-dojo-prompt">Scene prompt</label>
26
- <textarea id="kimodo-dojo-prompt" class="kimodo-dojo-prompt" rows="5"
27
- spellcheck="true">A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.</textarea>
28
 
29
- <div class="kimodo-dojo-presets" aria-label="Dojo scene presets">
30
- <button class="kimodo-dojo-preset" type="button"
31
- data-prompt="A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.">Classic dojo</button>
32
- <button class="kimodo-dojo-preset" type="button"
33
- data-prompt="An outdoor mountain dojo courtyard at sunrise, stone tiles, wooden gates, misty trees, full environment visible, high detail, no people, no text.">Mountain dojo</button>
34
- <button class="kimodo-dojo-preset" type="button"
35
- data-prompt="A futuristic neon martial arts training room, glossy floor, holographic wall panels, dramatic rim light, full room visible, no people, no text.">Neon dojo</button>
36
- </div>
37
 
38
- <div class="kimodo-dojo-grid">
39
- <label>
40
- <span>Seed</span>
41
- <input id="kimodo-dojo-seed" type="number" value="-1" step="1" />
42
- </label>
43
- <label>
44
- <span>Steps</span>
45
- <input id="kimodo-dojo-steps" type="number" value="28" min="1" max="100" step="1" />
46
- </label>
47
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- <div class="kimodo-dojo-actions">
50
- <button id="kimodo-dojo-generate" class="kimodo-dojo-primary" type="button">Generate Scene</button>
51
- <button id="kimodo-dojo-clear" type="button">Clear</button>
52
- </div>
53
 
54
- <div id="kimodo-dojo-progress" class="kimodo-dojo-progress" aria-label="Dojo generation progress">
55
- <div class="kimodo-dojo-step" data-step="flux-iso"><span></span>Flux isometric</div>
56
- <div class="kimodo-dojo-step" data-step="flux-pano"><span></span>Flux 360</div>
57
- <div class="kimodo-dojo-step" data-step="splat"><span></span>TripoSplat</div>
58
- <div class="kimodo-dojo-step" data-step="viewer"><span></span>Viewer load</div>
59
- </div>
60
- <div id="kimodo-dojo-time" class="kimodo-dojo-time" aria-live="polite"></div>
61
- <div id="kimodo-dojo-detail" class="kimodo-dojo-detail" aria-live="polite"></div>
62
- <div id="kimodo-dojo-preview-wrap" class="kimodo-dojo-preview-wrap">
63
- <figure>
64
- <figcaption>Isometric</figcaption>
65
- <img id="kimodo-dojo-preview" alt="Flux isometric source result" />
66
- </figure>
67
- <figure>
68
- <figcaption>360</figcaption>
69
- <img id="kimodo-dojo-preview-pano" alt="Flux 360 source result" />
70
- </figure>
71
  </div>
72
- <div id="kimodo-dojo-status" class="kimodo-drawer-sub kimodo-dojo-status">Ready.</div>
73
  </aside>
 
1
+ <aside id="kimodo-dojo-drawer" class="kimodo-drawer" aria-label="Dojo drawer">
2
  <h2>Dojo</h2>
 
3
 
4
+ <!-- ============ LIBRARY VIEW: list of dojos (Mine / Community) ============ -->
5
+ <div id="kimodo-dojo-library" class="kimodo-dojo-view">
6
+ <div class="kimodo-cz-seg kimodo-cz-seg--sub" id="kimodo-dojo-scope" aria-label="Dojo scope">
7
+ <button type="button" class="kimodo-cz-segbtn active" data-scope="mine">Mine</button>
8
+ <button type="button" class="kimodo-cz-segbtn" data-scope="community">Community</button>
9
+ </div>
10
+ <button id="kimodo-dojo-add" class="kimodo-dojo-addbtn" type="button">+ Add dojo</button>
11
+ <div id="kimodo-dojo-grid" class="kimodo-dojo-cards" aria-label="Dojo list"></div>
12
+ <div id="kimodo-dojo-empty" class="kimodo-drawer-sub kimodo-dojo-empty"></div>
13
  </div>
14
 
15
+ <!-- ============ CREATE VIEW: prompt + suggest + generate + progress ======= -->
16
+ <div id="kimodo-dojo-create" class="kimodo-dojo-view" hidden>
17
+ <button id="kimodo-dojo-back" class="kimodo-dojo-back" type="button" title="Back to dojos">‹ Dojos</button>
18
+ <div class="kimodo-drawer-sub">Describe a room scene for the viewer.</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ <label class="kimodo-dojo-label" for="kimodo-dojo-prompt">Scene prompt</label>
21
+ <textarea id="kimodo-dojo-prompt" class="kimodo-dojo-prompt" rows="5"
22
+ spellcheck="true" placeholder="A few words, or leave blank and tap Suggest…">A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.</textarea>
23
+ <button id="kimodo-dojo-improve" class="kimodo-dojo-improve" type="button"
24
+ title="Improve the prompt with tiny-aya (or suggest one if empty)"> Improve with tiny-aya</button>
 
 
 
25
 
26
+ <details class="kimodo-dojo-adv">
27
+ <summary>Advanced settings</summary>
28
+ <div class="kimodo-dojo-adv-body">
29
+ <div class="kimodo-cz-seg kimodo-dojo-mode" id="kimodo-dojo-mode" aria-label="Dojo scene type">
30
+ <button type="button" class="kimodo-cz-segbtn active" data-mode="splat">Splat</button>
31
+ <button type="button" class="kimodo-cz-segbtn" data-mode="panorama">Panorama</button>
32
+ </div>
33
+ <div class="kimodo-dojo-settings" aria-label="Dojo room settings">
34
+ <label class="kimodo-dojo-check">
35
+ <input id="kimodo-dojo-skybox" type="checkbox" />
36
+ <span>Use panorama as skybox in splat</span>
37
+ </label>
38
+ <label class="kimodo-dojo-range" for="kimodo-dojo-room-size">
39
+ <span>Room size <output id="kimodo-dojo-room-size-value">8.0</output>m</span>
40
+ <input id="kimodo-dojo-room-size" type="range" min="3" max="14" value="8" step="0.5" />
41
+ </label>
42
+ <label class="kimodo-dojo-range" for="kimodo-dojo-room-height">
43
+ <span>Room height <output id="kimodo-dojo-room-height-value">3.2</output>m</span>
44
+ <input id="kimodo-dojo-room-height" type="range" min="2" max="6" value="3.2" step="0.2" />
45
+ </label>
46
+ </div>
47
+ <div class="kimodo-dojo-grid2">
48
+ <label><span>Seed</span><input id="kimodo-dojo-seed" type="number" value="-1" step="1" /></label>
49
+ <label><span>Steps</span><input id="kimodo-dojo-steps" type="number" value="28" min="1" max="100" step="1" /></label>
50
+ </div>
51
+ </div>
52
+ </details>
53
 
54
+ <div class="kimodo-dojo-actions">
55
+ <button id="kimodo-dojo-generate" class="kimodo-dojo-primary" type="button">Generate Scene</button>
56
+ <button id="kimodo-dojo-clear" type="button">Clear</button>
57
+ </div>
58
 
59
+ <div id="kimodo-dojo-progress" class="kimodo-dojo-progress" aria-label="Dojo generation progress">
60
+ <div class="kimodo-dojo-step" data-step="flux-iso"><span></span>Flux isometric</div>
61
+ <div class="kimodo-dojo-step" data-step="flux-pano"><span></span>Flux 360</div>
62
+ <div class="kimodo-dojo-step" data-step="splat"><span></span>TripoSplat</div>
63
+ <div class="kimodo-dojo-step" data-step="viewer"><span></span>Viewer load</div>
64
+ </div>
65
+ <div id="kimodo-dojo-time" class="kimodo-dojo-time" aria-live="polite"></div>
66
+ <div id="kimodo-dojo-detail" class="kimodo-dojo-detail" aria-live="polite"></div>
67
+ <div id="kimodo-dojo-preview-wrap" class="kimodo-dojo-preview-wrap">
68
+ <figure><figcaption>Isometric</figcaption><img id="kimodo-dojo-preview" alt="Flux isometric result" /></figure>
69
+ <figure><figcaption>360</figcaption><img id="kimodo-dojo-preview-pano" alt="Flux 360 result" /></figure>
70
+ </div>
71
+ <div id="kimodo-dojo-status" class="kimodo-drawer-sub kimodo-dojo-status">Ready.</div>
 
 
 
 
72
  </div>
 
73
  </aside>
tabs/dojo.tab.css CHANGED
@@ -101,19 +101,19 @@
101
  .kimodo-dojo-actions button:hover:not(:disabled) {
102
  background: #3f3f48 !important;
103
  }
104
- .kimodo-dojo-grid {
105
  display: grid;
106
  grid-template-columns: 1fr 1fr;
107
  gap: 8px;
108
- margin: 10px 0;
109
  }
110
- .kimodo-dojo-grid label {
111
  display: grid;
112
  gap: 4px;
113
  font-size: 11px;
114
  color: #b8bec6;
115
  }
116
- .kimodo-dojo-grid input {
117
  width: 100%;
118
  box-sizing: border-box;
119
  padding: 6px 7px;
@@ -226,3 +226,137 @@
226
  aspect-ratio: 1 / 1;
227
  object-fit: cover;
228
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  .kimodo-dojo-actions button:hover:not(:disabled) {
102
  background: #3f3f48 !important;
103
  }
104
+ .kimodo-dojo-grid2 {
105
  display: grid;
106
  grid-template-columns: 1fr 1fr;
107
  gap: 8px;
108
+ margin: 10px 0 2px;
109
  }
110
+ .kimodo-dojo-grid2 label {
111
  display: grid;
112
  gap: 4px;
113
  font-size: 11px;
114
  color: #b8bec6;
115
  }
116
+ .kimodo-dojo-grid2 input {
117
  width: 100%;
118
  box-sizing: border-box;
119
  padding: 6px 7px;
 
226
  aspect-ratio: 1 / 1;
227
  object-fit: cover;
228
  }
229
+
230
+ /* ============ Library / create restructure ============ */
231
+ .kimodo-dojo-view[hidden] { display: none; }
232
+ .kimodo-dojo-addbtn {
233
+ width: 100%;
234
+ padding: 9px;
235
+ margin-bottom: 10px;
236
+ font-size: 12.5px;
237
+ font-weight: 600;
238
+ cursor: pointer;
239
+ border-radius: 8px;
240
+ background: #4a3320 !important;
241
+ color: #ffe0ad !important;
242
+ border: 1px solid #b97736 !important;
243
+ box-shadow: none !important;
244
+ }
245
+ .kimodo-dojo-addbtn:hover { background: #5a3f27 !important; }
246
+ .kimodo-dojo-cards {
247
+ display: grid;
248
+ grid-template-columns: 1fr 1fr;
249
+ gap: 8px;
250
+ }
251
+ .kimodo-dojo-card {
252
+ position: relative;
253
+ cursor: pointer;
254
+ border-radius: 8px;
255
+ overflow: hidden;
256
+ background: #202026;
257
+ border: 1px solid #34343c;
258
+ }
259
+ .kimodo-dojo-card:hover { border-color: #b97736; }
260
+ .kimodo-dojo-card .thumb {
261
+ display: block;
262
+ width: 100%;
263
+ aspect-ratio: 1 / 1;
264
+ object-fit: cover;
265
+ background: #15151a;
266
+ }
267
+ .kimodo-dojo-card .ph {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: center;
271
+ width: 100%;
272
+ aspect-ratio: 1 / 1;
273
+ font-size: 26px;
274
+ color: #6a5a3a;
275
+ background: #1b1b20;
276
+ }
277
+ .kimodo-dojo-card .nm {
278
+ padding: 5px 7px;
279
+ font-size: 10.5px;
280
+ line-height: 1.25;
281
+ color: #d7d3c8;
282
+ white-space: nowrap;
283
+ overflow: hidden;
284
+ text-overflow: ellipsis;
285
+ }
286
+ .kimodo-dojo-card .badge {
287
+ position: absolute;
288
+ left: 5px;
289
+ top: 5px;
290
+ padding: 1px 5px;
291
+ border-radius: 4px;
292
+ font-size: 8.5px;
293
+ font-weight: 700;
294
+ letter-spacing: .03em;
295
+ background: rgba(0, 0, 0, 0.6);
296
+ color: #ffd28a;
297
+ }
298
+ .kimodo-dojo-card .del {
299
+ position: absolute;
300
+ top: 4px;
301
+ right: 5px;
302
+ padding: 1px 5px;
303
+ font-size: 12px;
304
+ line-height: 1;
305
+ cursor: pointer;
306
+ color: #ccc !important;
307
+ background: rgba(0, 0, 0, 0.55) !important;
308
+ border: 0 !important;
309
+ border-radius: 5px;
310
+ box-shadow: none !important;
311
+ }
312
+ .kimodo-dojo-card .del:hover { color: #ff9a9a !important; }
313
+ .kimodo-dojo-empty {
314
+ margin-top: 12px;
315
+ text-align: center;
316
+ color: #87909b;
317
+ }
318
+ .kimodo-dojo-back {
319
+ padding: 4px 2px;
320
+ margin-bottom: 4px;
321
+ font-size: 13px;
322
+ cursor: pointer;
323
+ color: #ffd28a !important;
324
+ background: none !important;
325
+ border: 0 !important;
326
+ box-shadow: none !important;
327
+ }
328
+ .kimodo-dojo-improve {
329
+ width: 100%;
330
+ margin: 7px 0 4px;
331
+ padding: 7px;
332
+ font-size: 11.5px;
333
+ font-weight: 600;
334
+ cursor: pointer;
335
+ border-radius: 7px;
336
+ background: #2a2440 !important;
337
+ color: #cbb6ff !important;
338
+ border: 1px solid #4a3a6a !important;
339
+ box-shadow: none !important;
340
+ }
341
+ .kimodo-dojo-improve:hover:not(:disabled) { background: #342c52 !important; }
342
+ .kimodo-dojo-improve:disabled { opacity: .55; cursor: default; }
343
+ .kimodo-dojo-adv {
344
+ margin: 8px 0;
345
+ border: 1px solid #34343c;
346
+ border-radius: 7px;
347
+ background: #1d1d22;
348
+ overflow: hidden;
349
+ }
350
+ .kimodo-dojo-adv > summary {
351
+ padding: 8px 10px;
352
+ font-size: 11.5px;
353
+ font-weight: 600;
354
+ color: #b8bec6;
355
+ cursor: pointer;
356
+ list-style: none;
357
+ }
358
+ .kimodo-dojo-adv > summary::-webkit-details-marker { display: none; }
359
+ .kimodo-dojo-adv > summary::after { content: " ▾"; color: #7f8792; }
360
+ .kimodo-dojo-adv[open] > summary::after { content: " ▴"; }
361
+ .kimodo-dojo-adv-body { padding: 0 10px 8px; }
362
+ .kimodo-dojo-adv-body .kimodo-dojo-mode { margin-top: 2px; }
tabs/dojo.tab.js CHANGED
@@ -2,9 +2,29 @@
2
  let busy = false;
3
  let dojoMode = 'splat';
4
  let lastScene = null;
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  function dojoEls() {
7
  return {
 
 
 
 
 
 
 
 
8
  prompt: document.getElementById('kimodo-dojo-prompt'),
9
  mode: document.getElementById('kimodo-dojo-mode'),
10
  roomSize: document.getElementById('kimodo-dojo-room-size'),
@@ -91,10 +111,7 @@
91
  if (els.detail) els.detail.textContent = bestProgressText(stage);
92
  }
93
 
94
- function hiddenInput(id) {
95
- return inputInside(id);
96
- }
97
-
98
  function writeHidden(id, value) {
99
  const input = hiddenInput(id);
100
  if (!input) return false;
@@ -103,12 +120,20 @@
103
  input.dispatchEvent(new Event('change', { bubbles: true }));
104
  return true;
105
  }
106
-
107
  function readHidden(id) {
108
  const input = hiddenInput(id);
109
  return input ? input.value || '' : '';
110
  }
111
 
 
 
 
 
 
 
 
 
 
112
  function sendScene(scene) {
113
  const frame = previewFrame();
114
  if (!scene) return;
@@ -119,15 +144,6 @@
119
  if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-scene', scene: payload, image: payload.image }, '*');
120
  }
121
 
122
- function roomSettings() {
123
- const els = dojoEls();
124
- return {
125
- room_size: Math.max(1, Number(els.roomSize && els.roomSize.value) || 8),
126
- room_height: Math.max(1, Number(els.roomHeight && els.roomHeight.value) || 3.2),
127
- use_panorama_skybox: !!(els.skybox && els.skybox.checked)
128
- };
129
- }
130
-
131
  function updateRoomLabels() {
132
  const els = dojoEls();
133
  const settings = roomSettings();
@@ -172,6 +188,7 @@
172
  const els = dojoEls();
173
  if (els.generate) els.generate.disabled = busy;
174
  if (els.clear) els.clear.disabled = busy;
 
175
  }
176
 
177
  function waitForResult(previousValue, onStage) {
@@ -213,144 +230,280 @@
213
  };
214
  }
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  function initDojo() {
217
  const drawer = document.getElementById('kimodo-dojo-drawer');
218
  const els = dojoEls();
219
- if (!drawer || drawer.dataset.ready === '1') return;
220
  drawer.dataset.ready = '1';
221
  updateRoomLabels();
 
222
 
223
- if (els.mode) {
224
- els.mode.addEventListener('click', (e) => {
225
- const button = e.target.closest('.kimodo-cz-segbtn[data-mode]');
226
- if (!button || busy) return;
227
- dojoMode = button.dataset.mode || 'splat';
228
- els.mode.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b === button));
229
- setStatus(dojoMode === 'panorama' ? 'Panorama mode selected.' : 'Splat mode selected.');
230
- if (lastScene) sendScene(lastScene);
231
- });
232
- }
 
 
 
 
 
 
 
 
233
 
234
  ['roomSize', 'roomHeight', 'skybox'].forEach((key) => {
235
  const input = els[key];
236
  if (!input) return;
237
- input.addEventListener('input', () => {
238
- updateRoomLabels();
239
- sendRoomSettings();
240
- });
241
- input.addEventListener('change', () => {
242
- updateRoomLabels();
243
- if (lastScene) sendScene(lastScene);
244
- });
245
  });
246
 
247
- drawer.querySelectorAll('.kimodo-dojo-preset').forEach((button) => {
248
- button.addEventListener('click', () => {
249
- if (els.prompt) {
250
- els.prompt.value = button.dataset.prompt || '';
251
- els.prompt.dispatchEvent(new Event('input', { bubbles: true }));
252
- }
253
- setStatus('Preset loaded.');
254
- });
255
  });
256
-
257
- if (els.generate) {
258
- els.generate.addEventListener('click', async () => {
259
- if (busy) return;
260
- const cfg = currentConfig();
261
- if (!cfg.prompt) {
262
- setStatus('Add a scene prompt first.');
263
- return;
264
- }
265
- const payloadInput = hiddenInput('dojo-payload');
266
- const trigger = document.querySelector('#dojo-btn button, #dojo-btn');
267
- if (!payloadInput || !trigger) {
268
- setStatus('Dojo backend controls are missing.');
269
- return;
270
- }
271
- setBusy(true);
272
- resetProgress();
273
- setStep('flux-iso', 'active');
274
- setStatus('Starting isometric Flux generation...');
275
- writeHidden('dojo-result', '');
276
- const previous = '';
277
- writeHidden('dojo-payload', JSON.stringify(cfg));
278
- try {
279
- trigger.click();
280
- const value = await waitForResult(previous, (stage) => {
281
- setTiming(stage);
282
- if (stage.source_image || stage.isometric_image) showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
283
- if (stage.panorama_image) showSourceImage(stage.panorama_image, 'panorama');
284
- if (stage.stage === 'flux-iso-start') {
285
- setStep('flux-iso', 'active');
286
- setStatus(stage.status || 'Generating isometric Flux image...');
287
- } else if (stage.stage === 'flux-iso-progress') {
288
- setStep('flux-iso', 'active');
289
- setStatus(stage.status || 'Generating isometric Flux image...');
290
- } else if (stage.stage === 'flux-iso-done') {
291
- setStep('flux-iso', 'done');
292
- setStep('flux-pano', 'active');
293
- showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
294
- setStatus(stage.status || 'Isometric Flux image ready. Starting 360 Flux image...');
295
- } else if (stage.stage === 'flux-pano-start') {
296
- setStep('flux-iso', 'done');
297
- setStep('flux-pano', 'active');
298
- setStatus(stage.status || 'Generating 360 Flux image...');
299
- } else if (stage.stage === 'flux-pano-progress') {
300
- setStep('flux-pano', 'active');
301
- setStatus(stage.status || 'Generating 360 Flux image...');
302
- } else if (stage.stage === 'flux-pano-done') {
303
- setStep('flux-pano', 'done');
304
- setStep('splat', 'active');
305
- showSourceImage(stage.panorama_image, 'panorama');
306
- setStatus(stage.status || '360 Flux image ready. Starting TripoSplat...');
307
- } else if (stage.stage === 'splat-start') {
308
- setStep('splat', 'active');
309
- setStatus(stage.status || 'Generating TripoSplat...');
310
- } else if (stage.stage === 'splat-progress') {
311
- setStep('flux-iso', 'done');
312
- setStep('flux-pano', 'done');
313
- setStep('splat', 'active');
314
- setStatus(bestProgressText(stage) || stage.status || 'Generating TripoSplat...');
315
- } else if (stage.stage === 'splat-done') {
316
- setStep('splat', 'done');
317
- setStep('viewer', 'active');
318
- setStatus(stage.status || 'TripoSplat ready. Loading viewer...');
319
- } else if (stage.stage === 'fallback') {
320
- setStep('flux-iso', 'fail');
321
- setStep('flux-pano', 'fail');
322
- setStep('splat', 'fail');
323
- setStatus(stage.warning || stage.error || 'Fallback generated.');
324
- }
325
- });
326
- const scene = JSON.parse(value);
327
- if (scene && (scene.image || scene.splat || scene.splat_url)) {
328
- Object.assign(scene, roomSettings());
329
- lastScene = scene;
330
- sendScene(scene);
331
- if (scene.source === 'klein-triposplat') setStatus((dojoMode === 'panorama' ? 'Panorama applied. ' : 'Splat applied. ') + (scene.info || ''));
332
- else if (scene.source === 'dit360') setStatus(scene.stage === 'fallback' ? 'Panorama fallback applied.' : 'Panorama applied.');
333
- else setStatus((scene.warning || 'Using default scene.') + ' Default scene applied.');
334
- } else {
335
- setStatus((scene && scene.error) || 'Scene generation failed.');
336
- }
337
- } catch (e) {
338
- console.error('dojo scene generation failed', e);
339
- setStatus('Scene generation failed: ' + e.message);
340
- } finally {
341
- setBusy(false);
342
- }
343
- });
344
- }
345
-
346
- if (els.clear) {
347
- els.clear.addEventListener('click', () => {
348
- const frame = previewFrame();
349
- if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-clear' }, '*');
350
- resetProgress();
351
- setStatus('Scene cleared.');
352
- });
353
- }
354
  }
355
 
356
  window.addEventListener('kimodo-drawer-open', (e) => {
 
2
  let busy = false;
3
  let dojoMode = 'splat';
4
  let lastScene = null;
5
+ let dojoScope = 'mine';
6
+
7
+ // Community starters — tapping one opens the create view prefilled, so generating
8
+ // produces a real flux + splat scene (which then lands under "Mine").
9
+ const COMMUNITY_SEED = [
10
+ { name: 'Classic dojo', mode: 'splat', settings: { room_size: 8, room_height: 3.2 },
11
+ prompt: 'A traditional martial arts dojo room, polished wooden floor, training mats, wooden wall panels, warm lantern light, full room visible, high detail, no people, no text.' },
12
+ { name: 'Mountain dojo', mode: 'panorama', settings: { room_size: 10, room_height: 4 },
13
+ prompt: 'An outdoor mountain dojo courtyard at sunrise, stone tiles, wooden gates, misty trees, full environment visible, high detail, no people, no text.' },
14
+ { name: 'Neon dojo', mode: 'splat', settings: { room_size: 8, room_height: 3.2 },
15
+ prompt: 'A futuristic neon martial arts training room, glossy floor, holographic wall panels, dramatic rim light, full room visible, no people, no text.' }
16
+ ];
17
 
18
  function dojoEls() {
19
  return {
20
+ library: document.getElementById('kimodo-dojo-library'),
21
+ create: document.getElementById('kimodo-dojo-create'),
22
+ scope: document.getElementById('kimodo-dojo-scope'),
23
+ add: document.getElementById('kimodo-dojo-add'),
24
+ back: document.getElementById('kimodo-dojo-back'),
25
+ grid: document.getElementById('kimodo-dojo-grid'),
26
+ emptyMsg: document.getElementById('kimodo-dojo-empty'),
27
+ improve: document.getElementById('kimodo-dojo-improve'),
28
  prompt: document.getElementById('kimodo-dojo-prompt'),
29
  mode: document.getElementById('kimodo-dojo-mode'),
30
  roomSize: document.getElementById('kimodo-dojo-room-size'),
 
111
  if (els.detail) els.detail.textContent = bestProgressText(stage);
112
  }
113
 
114
+ function hiddenInput(id) { return inputInside(id); }
 
 
 
115
  function writeHidden(id, value) {
116
  const input = hiddenInput(id);
117
  if (!input) return false;
 
120
  input.dispatchEvent(new Event('change', { bubbles: true }));
121
  return true;
122
  }
 
123
  function readHidden(id) {
124
  const input = hiddenInput(id);
125
  return input ? input.value || '' : '';
126
  }
127
 
128
+ function roomSettings() {
129
+ const els = dojoEls();
130
+ return {
131
+ room_size: Math.max(1, Number(els.roomSize && els.roomSize.value) || 8),
132
+ room_height: Math.max(1, Number(els.roomHeight && els.roomHeight.value) || 3.2),
133
+ use_panorama_skybox: !!(els.skybox && els.skybox.checked)
134
+ };
135
+ }
136
+
137
  function sendScene(scene) {
138
  const frame = previewFrame();
139
  if (!scene) return;
 
144
  if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-scene', scene: payload, image: payload.image }, '*');
145
  }
146
 
 
 
 
 
 
 
 
 
 
147
  function updateRoomLabels() {
148
  const els = dojoEls();
149
  const settings = roomSettings();
 
188
  const els = dojoEls();
189
  if (els.generate) els.generate.disabled = busy;
190
  if (els.clear) els.clear.disabled = busy;
191
+ if (els.improve) els.improve.disabled = busy;
192
  }
193
 
194
  function waitForResult(previousValue, onStage) {
 
230
  };
231
  }
232
 
233
+ // ---------- Saved dojos (localStorage) ----------
234
+ function loadMyDojos() { try { return JSON.parse(localStorage.getItem('kimodoMyDojos') || '[]'); } catch (e) { return []; } }
235
+ function saveMyDojos(list) {
236
+ let arr = (list || []).slice(0, 16);
237
+ // Scene payloads carry data-URL splats (can be MBs); drop the oldest until it fits.
238
+ while (arr.length) {
239
+ try { localStorage.setItem('kimodoMyDojos', JSON.stringify(arr)); return true; }
240
+ catch (e) { arr = arr.slice(0, arr.length - 1); }
241
+ }
242
+ try { localStorage.setItem('kimodoMyDojos', '[]'); } catch (e) {}
243
+ return false;
244
+ }
245
+ function dojoName(prompt) {
246
+ const head = (prompt || '').replace(/[.,].*$/, '').trim().replace(/^(a|an|the)\s+/i, '');
247
+ const name = head.split(/\s+/).slice(0, 4).join(' ');
248
+ return (name || 'Dojo').slice(0, 30);
249
+ }
250
+ function saveGeneratedDojo(scene, cfg) {
251
+ const thumb = scene.isometric_image || scene.source_image || scene.image || '';
252
+ const entry = {
253
+ id: 'd' + Date.now().toString(36),
254
+ name: dojoName(cfg.prompt),
255
+ prompt: cfg.prompt,
256
+ mode: cfg.mode,
257
+ settings: { room_size: cfg.room_size, room_height: cfg.room_height, use_panorama_skybox: !!cfg.use_panorama_skybox, seed: cfg.seed, steps: cfg.steps },
258
+ scene: scene,
259
+ thumb: thumb,
260
+ ts: Date.now()
261
+ };
262
+ const arr = loadMyDojos();
263
+ arr.unshift(entry);
264
+ saveMyDojos(arr);
265
+ return entry;
266
+ }
267
+
268
+ // ---------- View + library ----------
269
+ function showView(view) {
270
+ const els = dojoEls();
271
+ if (els.library) els.library.hidden = (view !== 'library');
272
+ if (els.create) els.create.hidden = (view !== 'create');
273
+ if (view === 'library') renderLibrary();
274
+ }
275
+
276
+ function syncModeButtons() {
277
+ const els = dojoEls();
278
+ if (!els.mode) return;
279
+ els.mode.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.mode === dojoMode));
280
+ }
281
+
282
+ function applySettings(settings) {
283
+ const els = dojoEls();
284
+ if (!settings) return;
285
+ if (els.roomSize && settings.room_size) els.roomSize.value = settings.room_size;
286
+ if (els.roomHeight && settings.room_height) els.roomHeight.value = settings.room_height;
287
+ if (els.skybox) els.skybox.checked = !!settings.use_panorama_skybox;
288
+ if (els.seed && settings.seed != null) els.seed.value = settings.seed;
289
+ if (els.steps && settings.steps != null) els.steps.value = settings.steps;
290
+ updateRoomLabels();
291
+ }
292
+
293
+ function openCreate(seed) {
294
+ showView('create');
295
+ resetProgress();
296
+ const els = dojoEls();
297
+ if (seed) {
298
+ if (els.prompt) { els.prompt.value = seed.prompt || ''; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); }
299
+ dojoMode = seed.mode || 'splat';
300
+ syncModeButtons();
301
+ applySettings(seed.settings);
302
+ setStatus('Loaded "' + (seed.name || 'starter') + '". Tweak and Generate.');
303
+ } else {
304
+ setStatus('Ready.');
305
+ }
306
+ }
307
+
308
+ function mineCard(entry) {
309
+ const card = document.createElement('div');
310
+ card.className = 'kimodo-dojo-card';
311
+ card.title = entry.name || 'dojo';
312
+ if (entry.thumb) {
313
+ const img = document.createElement('img');
314
+ img.className = 'thumb'; img.src = entry.thumb; img.alt = entry.name || 'dojo';
315
+ card.appendChild(img);
316
+ } else {
317
+ const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph);
318
+ }
319
+ const tag = document.createElement('span'); tag.className = 'badge'; tag.textContent = (entry.mode === 'panorama' ? '360' : 'SPLAT'); card.appendChild(tag);
320
+ const del = document.createElement('button'); del.className = 'del'; del.type = 'button'; del.textContent = '✕'; del.title = 'Delete';
321
+ del.onclick = (e) => { e.stopPropagation(); if (!window.confirm('Delete this dojo?')) return; saveMyDojos(loadMyDojos().filter((x) => x.id !== entry.id)); renderLibrary(); };
322
+ card.appendChild(del);
323
+ const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = entry.name || 'Dojo'; card.appendChild(nm);
324
+ card.onclick = () => applySavedDojo(entry);
325
+ return card;
326
+ }
327
+
328
+ function communityCard(seed) {
329
+ const card = document.createElement('div');
330
+ card.className = 'kimodo-dojo-card';
331
+ card.title = seed.name;
332
+ const ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = '🏯'; card.appendChild(ph);
333
+ const tag = document.createElement('span'); tag.className = 'badge'; tag.textContent = 'STARTER'; card.appendChild(tag);
334
+ const nm = document.createElement('div'); nm.className = 'nm'; nm.textContent = seed.name; card.appendChild(nm);
335
+ card.onclick = () => openCreate(seed);
336
+ return card;
337
+ }
338
+
339
+ function renderLibrary() {
340
+ const els = dojoEls();
341
+ if (els.scope) els.scope.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === dojoScope));
342
+ if (!els.grid) return;
343
+ els.grid.innerHTML = '';
344
+ if (dojoScope === 'mine') {
345
+ const arr = loadMyDojos();
346
+ arr.forEach((e) => els.grid.appendChild(mineCard(e)));
347
+ if (els.emptyMsg) els.emptyMsg.textContent = arr.length ? '' : 'No dojos yet — tap + Add dojo to create one.';
348
+ } else {
349
+ COMMUNITY_SEED.forEach((s) => els.grid.appendChild(communityCard(s)));
350
+ if (els.emptyMsg) els.emptyMsg.textContent = '';
351
+ }
352
+ }
353
+
354
+ function applySavedDojo(entry) {
355
+ const scene = entry.scene;
356
+ if (!scene) { openCreate(entry); return; } // recipe-only fallback → re-generate
357
+ dojoMode = scene.render_mode || (scene.splat || scene.splat_url ? 'splat' : 'panorama');
358
+ syncModeButtons();
359
+ applySettings(entry.settings);
360
+ lastScene = scene;
361
+ sendScene(scene);
362
+ setStatus('Applied "' + (entry.name || 'dojo') + '".');
363
+ }
364
+
365
+ // ---------- tiny-aya prompt suggestion ----------
366
+ function improvePrompt() {
367
+ const els = dojoEls();
368
+ const payloadInput = hiddenInput('dojo-suggest-payload');
369
+ const trigger = document.querySelector('#dojo-suggest-btn button, #dojo-suggest-btn');
370
+ if (!payloadInput || !trigger) { setStatus('Suggest backend missing.'); return; }
371
+ const words = (els.prompt && els.prompt.value || '').trim();
372
+ if (els.improve) { els.improve.disabled = true; els.improve.textContent = '✦ Thinking…'; }
373
+ setStatus(words ? 'Improving your prompt with tiny-aya…' : 'Suggesting a prompt with tiny-aya…');
374
+ writeHidden('dojo-suggest-result', '');
375
+ writeHidden('dojo-suggest-payload', JSON.stringify({ words: words }));
376
+ const started = Date.now();
377
+ const timer = window.setInterval(() => {
378
+ const val = readHidden('dojo-suggest-result');
379
+ if (val) { window.clearInterval(timer); finishImprove(val); }
380
+ else if (Date.now() - started > 150000) { window.clearInterval(timer); finishImprove('{"ok":false,"error":"timed out"}'); }
381
+ }, 300);
382
+ window.setTimeout(() => trigger.click(), 40);
383
+
384
+ function finishImprove(val) {
385
+ if (els.improve) { els.improve.disabled = busy; els.improve.textContent = '✦ Improve with tiny-aya'; }
386
+ try {
387
+ const r = JSON.parse(val);
388
+ if (r.ok && r.prompt) {
389
+ if (els.prompt) { els.prompt.value = r.prompt; els.prompt.dispatchEvent(new Event('input', { bubbles: true })); }
390
+ setStatus('Prompt suggested ✦');
391
+ } else {
392
+ setStatus('Suggest failed: ' + (r.error || 'unknown'));
393
+ }
394
+ } catch (e) { setStatus('Suggest failed.'); }
395
+ }
396
+ }
397
+
398
+ // ---------- Generate ----------
399
+ async function runGenerate() {
400
+ if (busy) return;
401
+ const cfg = currentConfig();
402
+ if (!cfg.prompt) { setStatus('Add a scene prompt first (or tap Improve).'); return; }
403
+ const payloadInput = hiddenInput('dojo-payload');
404
+ const trigger = document.querySelector('#dojo-btn button, #dojo-btn');
405
+ if (!payloadInput || !trigger) { setStatus('Dojo backend controls are missing.'); return; }
406
+ setBusy(true);
407
+ resetProgress();
408
+ setStep('flux-iso', 'active');
409
+ setStatus('Starting isometric Flux generation...');
410
+ writeHidden('dojo-result', '');
411
+ writeHidden('dojo-payload', JSON.stringify(cfg));
412
+ try {
413
+ trigger.click();
414
+ const value = await waitForResult('', (stage) => {
415
+ setTiming(stage);
416
+ if (stage.source_image || stage.isometric_image) showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
417
+ if (stage.panorama_image) showSourceImage(stage.panorama_image, 'panorama');
418
+ if (stage.stage === 'flux-iso-start' || stage.stage === 'flux-iso-progress') {
419
+ setStep('flux-iso', 'active');
420
+ setStatus(stage.status || 'Generating isometric Flux image...');
421
+ } else if (stage.stage === 'flux-iso-done') {
422
+ setStep('flux-iso', 'done'); setStep('flux-pano', 'active');
423
+ showSourceImage(stage.source_image || stage.isometric_image, 'isometric');
424
+ setStatus(stage.status || 'Isometric ready. Starting 360 Flux...');
425
+ } else if (stage.stage === 'flux-pano-start' || stage.stage === 'flux-pano-progress') {
426
+ setStep('flux-iso', 'done'); setStep('flux-pano', 'active');
427
+ setStatus(stage.status || 'Generating 360 Flux image...');
428
+ } else if (stage.stage === 'flux-pano-done') {
429
+ setStep('flux-pano', 'done'); setStep('splat', 'active');
430
+ showSourceImage(stage.panorama_image, 'panorama');
431
+ setStatus(stage.status || '360 ready. Starting TripoSplat...');
432
+ } else if (stage.stage === 'splat-start' || stage.stage === 'splat-progress') {
433
+ setStep('flux-iso', 'done'); setStep('flux-pano', 'done'); setStep('splat', 'active');
434
+ setStatus(bestProgressText(stage) || stage.status || 'Generating TripoSplat...');
435
+ } else if (stage.stage === 'splat-done') {
436
+ setStep('splat', 'done'); setStep('viewer', 'active');
437
+ setStatus(stage.status || 'TripoSplat ready. Loading viewer...');
438
+ } else if (stage.stage === 'fallback') {
439
+ setStep('flux-iso', 'fail'); setStep('flux-pano', 'fail'); setStep('splat', 'fail');
440
+ setStatus(stage.warning || stage.error || 'Fallback generated.');
441
+ }
442
+ });
443
+ const scene = JSON.parse(value);
444
+ if (scene && (scene.image || scene.splat || scene.splat_url)) {
445
+ Object.assign(scene, roomSettings());
446
+ lastScene = scene;
447
+ sendScene(scene);
448
+ // A real scene (klein-triposplat / dit360) is saved under "Mine".
449
+ if (scene.source === 'klein-triposplat' || scene.source === 'dit360') {
450
+ const entry = saveGeneratedDojo(scene, cfg);
451
+ setStatus('Saved "' + entry.name + '" to My dojos. ' + (scene.info || ''));
452
+ } else {
453
+ setStatus((scene.warning || 'Using default scene.') + ' Default scene applied.');
454
+ }
455
+ } else {
456
+ setStatus((scene && scene.error) || 'Scene generation failed.');
457
+ }
458
+ } catch (e) {
459
+ console.error('dojo scene generation failed', e);
460
+ setStatus('Scene generation failed: ' + e.message);
461
+ } finally {
462
+ setBusy(false);
463
+ }
464
+ }
465
+
466
  function initDojo() {
467
  const drawer = document.getElementById('kimodo-dojo-drawer');
468
  const els = dojoEls();
469
+ if (!drawer || drawer.dataset.ready === '1') { renderLibrary(); return; }
470
  drawer.dataset.ready = '1';
471
  updateRoomLabels();
472
+ showView('library');
473
 
474
+ if (els.scope) els.scope.addEventListener('click', (e) => {
475
+ const b = e.target.closest('.kimodo-cz-segbtn[data-scope]');
476
+ if (!b) return;
477
+ dojoScope = b.dataset.scope || 'mine';
478
+ renderLibrary();
479
+ });
480
+ if (els.add) els.add.addEventListener('click', () => openCreate(null));
481
+ if (els.back) els.back.addEventListener('click', () => showView('library'));
482
+ if (els.improve) els.improve.addEventListener('click', () => { if (!busy) improvePrompt(); });
483
+
484
+ if (els.mode) els.mode.addEventListener('click', (e) => {
485
+ const button = e.target.closest('.kimodo-cz-segbtn[data-mode]');
486
+ if (!button || busy) return;
487
+ dojoMode = button.dataset.mode || 'splat';
488
+ syncModeButtons();
489
+ setStatus(dojoMode === 'panorama' ? 'Panorama mode selected.' : 'Splat mode selected.');
490
+ if (lastScene) sendScene(lastScene);
491
+ });
492
 
493
  ['roomSize', 'roomHeight', 'skybox'].forEach((key) => {
494
  const input = els[key];
495
  if (!input) return;
496
+ input.addEventListener('input', () => { updateRoomLabels(); sendRoomSettings(); });
497
+ input.addEventListener('change', () => { updateRoomLabels(); if (lastScene) sendScene(lastScene); });
 
 
 
 
 
 
498
  });
499
 
500
+ if (els.generate) els.generate.addEventListener('click', runGenerate);
501
+ if (els.clear) els.clear.addEventListener('click', () => {
502
+ const frame = previewFrame();
503
+ if (frame && frame.contentWindow) frame.contentWindow.postMessage({ kind: 'dojo-clear' }, '*');
504
+ resetProgress();
505
+ setStatus('Scene cleared.');
 
 
506
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  }
508
 
509
  window.addEventListener('kimodo-drawer-open', (e) => {