PageSourceSearch

https://smartcone.ai/src/tutorial.js

js smartcone.ai collected 2026-10-05 15:06:10 UTC 15,177 bytes, 426 lines download raw bytes

1/**
2 * tutorial.js — Interactive step-by-step tutorial delivered by the floating
3 * digital assistant.
4 *
5 * Usage:
6 *   import { initTutorial, startTutorial } from "./tutorial.js";
7 *   initTutorial();      // call once on app load (auto-starts for new users)
8 *   startTutorial();     // call to manually re-trigger
9 */
10
11import { openStreamManagerHelp } from "./streams/helpPopup.js";
12
13const TUTORIAL_KEY = "zimTutorialDone";
14
15/** Z-index layers: overlay (99990) → spotlight (99992) → card (99995) */
16const Z_SPOTLIGHT           = 99992;
17const MAX_SPEECH_LEN        = 90;
18const SPEECH_TRUNCATE_AT    = 87;
19/** ms to wait after app load before auto-starting (lets the assistant greet first) */
20const TUTORIAL_AUTO_START_DELAY = 5000;
21
22// ─── Tutorial steps ────────────────────────────────────────────────────────────
23const STEPS = [
24  {
25    title: "Welcome to ZimxDesigner! 👋",
26    text: "Hi! I'm your digital assistant. Let me give you a quick tour so you can start building amazing UIs right away!",
27    target: null,
28    topic: "intro",
29  },
30  {
31    title: "The Canvas 🖼️",
32    text: "This is your canvas — the main workspace. Drag and drop components here to build your design!",
33    target: "#canvas",
34    position: "center",
35    topic: "canvas",
36  },
37  {
38    title: "App Tray 🛠️",
39    text: "The App Tray at the bottom gives you quick access to all your tools: Designer, Whiteboard, Task Board, Mindmap, Calendar, and more!",
40    target: "#appTray",
41    position: "top",
42    topic: "apptray",
43  },
44  {
45    title: "Designer Palette 🎨",
46    text: "Click the Designer button to open the component palette. Drag any component from the palette onto the canvas to add it to your design!",
47    target: '[data-app-id="designer"]',
48    position: "top",
49    topic: "designer",
50  },
51  {
52    title: "Radial Menu 🌀",
53    text: "Right-click (or long-press) anywhere on the canvas to open the Radial Menu — a fast way to create containers, charts, controls and more!",
54    target: null,
55    position: "center",
56    topic: "radial",
57  },
58  {
59    title: "App Viewer 👁",
60    text: "Click App Viewer in the App Tray to preview your production build inside the studio! Set the Project & Layout names, then click ▶ Preview. Switch between Desktop, Tablet, and Mobile presets to test responsiveness.",
61    target: '[data-app-id="appviewer"]',
62    position: "top",
63    topic: "appviewer",
64  },
65  {
66    title: "Save & Load Your Work 💾",
67    text: "Click the Repo button to save or load your project. Your work is stored as an NDJSON file so you can come back to it anytime.",
68    target: ".app-tray-repo",
69    position: "top",
70    topic: "save",
71  },
72  {
73    title: "Animation Player 🎬",
74    text: "Add an Animation Player from the Designer palette to bring motion to your designs. It plays storyboard animations right on the canvas!",
75    target: '[data-app-id="designer"]',
76    position: "top",
77    topic: "animation",
78  },
79  {
80    title: "Create & Edit Animations ✏️",
81    text: "Click the ✏ Edit button on an Animation Player to open the Storyboard Editor. Draw shapes, set keyframes, and build frame-by-frame animations with full timeline control.",
82    target: null,
83    position: "center",
84    topic: "animation",
85  },
86  {
87    title: "Reload Your Animation ↺",
88    text: "After editing, close the Storyboard Editor and the player reloads automatically. You can also hit the ↺ Reload button at any time to refresh the animation from its saved asset.",
89    target: null,
90    position: "center",
91    topic: "animation",
92  },
93  {
94    title: "You're All Set! 🎉",
95    text: "That's the tour! Click on me anytime for tips and helpful hints. Happy designing — I'll be cheering you on! ✨",
96    target: null,
97    topic: "outro",
98  },
99];
100
101// ─── TutorialManager ──────────────────────────────────────────────────────────
102class TutorialManager {
103  constructor() {
104    this._step    = 0;
105    this._overlay = null;
106    this._card    = null;
107    this._spotEl  = null;
108    this._active  = false;
109  }
110
111  /** Start (or restart) the tutorial from step 0. */
112  start() {
113    this._teardown();
114    this._step   = 0;
115    this._active = true;
116    this._buildDOM();
117    this._showStep(0);
118  }
119
120  /** Start tutorial from a specific topic */
121  startFromTopic(topic) {
122    if (topic === "all") {
123      this.start();
124      return;
125    }
126
127    // Handle special documentation topics (not part of the tutorial flow)
128    if (topic === "database") {
129      // Open database tutorial documentation in a new window
130      window.open("/docs/zim/how-to-add-data-tutorial.md", "_blank");
131      return;
132    }
133
134    if (topic === "streams") {
135      // Open Stream Manager help popup
136      openStreamManagerHelp();
137      return;
138    }
139
140    // Find the step index for the topic
141    const stepIndex = STEPS.findIndex(step => step.topic === topic);
142    if (stepIndex === -1) {
143      this.start(); // fallback to full tutorial
144      return;
145    }
146
147    this._teardown();
148    this._step   = stepIndex;
149    this._active = true;
150    this._buildDOM();
151    this._showStep(stepIndex);
152  }
153
154  /** Close and mark completed. */
155  stop() {
156    this._teardown();
157    this._active = false;
158    // Reset assistant tutorial mode
159    const assistant = window.zimAssistant;
160    if (assistant) {
161      assistant.setTutorialMode(false);
162    }
163    try { localStorage.setItem(TUTORIAL_KEY, "1"); } catch { /* silent */ }
164  }
165
166  // ── DOM ──────────────────────────────────────────────────────────────────
167
168  _buildDOM() {
169    // Semi-transparent dim overlay
170    this._overlay = document.createElement("div");
171    this._overlay.id = "tutorialOverlay";
172    document.body.appendChild(this._overlay);
173
174    // Tutorial card
175    this._card = document.createElement("div");
176    this._card.id = "tutorialCard";
177    document.body.appendChild(this._card);
178  }
179
180  _teardown() {
181    this._clearSpotlight();
182    this._overlay?.remove();
183    this._card?.remove();
184    this._overlay = null;
185    this._card    = null;
186  }
187
188  // ── Spotlight ────────────────────────────────────────────────────────────
189
190  _applySpotlight(el) {
191    this._clearSpotlight();
192    if (!el) return;
193    this._spotEl = el;
194    el.classList.add("tutorial-spotlight");
195    // Boost z-index so element appears above the dim overlay
196    el._prevZIndex = el.style.zIndex;
197    el.style.setProperty("z-index", String(Z_SPOTLIGHT), "important");
198  }
199
200  _clearSpotlight() {
201    if (!this._spotEl) return;
202    this._spotEl.classList.remove("tutorial-spotlight");
203    this._spotEl.style.zIndex = this._spotEl._prevZIndex || "";
204    delete this._spotEl._prevZIndex;
205    this._spotEl = null;
206  }
207
208  // ── Render step ──────────────────────────────────────────────────────────
209
210  _showStep(idx) {
211    const step  = STEPS[idx];
212    if (!step) return;
213
214    const total   = STEPS.length;
215    const isFirst = idx === 0;
216    const isLast  = idx === total - 1;
217
218    // Spotlight target
219    const targetEl = step.target ? document.querySelector(step.target) : null;
220    this._applySpotlight(targetEl);
221
222    // Progress dots
223    const dots = STEPS.map((_, i) =>
224      `<span class="tutorial-dot${i === idx ? " active" : ""}"></span>`
225    ).join("");
226
227    // Card markup
228    this._card.innerHTML = `
229      <div class="tutorial-card-header">${step.title}</div>
230      <div class="tutorial-card-body">${step.text}</div>
231      <div class="tutorial-card-footer">
232        <div class="tutorial-dots">${dots}</div>
233        <div class="tutorial-btns">
234          ${!isFirst ? '<button class="tutorial-btn tutorial-btn-prev">← Back</button>' : ""}
235          <button class="tutorial-btn tutorial-btn-skip">Skip</button>
236          <button class="tutorial-btn tutorial-btn-next${isLast ? " primary" : ""}">
237            ${isLast ? "Finish 🎉" : "Next →"}
238          </button>
239        </div>
240      </div>`;
241
242    // Button listeners
243    this._card.querySelector(".tutorial-btn-next")
244      ?.addEventListener("click", () => isLast ? this._finish() : this._go(idx + 1));
245    this._card.querySelector(".tutorial-btn-prev")
246      ?.addEventListener("click", () => this._go(idx - 1));
247    this._card.querySelector(".tutorial-btn-skip")
248      ?.addEventListener("click", () => this.stop());
249
250    // Have the assistant speak the step text and position with card
251    const assistant = window.zimAssistant;
252    if (assistant) {
253      // Strip emoji for cleaner speech-bubble display
254      const speech = step.text.replace(/[\u{1F300}-\u{1FFFF}]/gu, "").trim();
255      assistant.say(speech.length > MAX_SPEECH_LEN ? speech.substring(0, SPEECH_TRUNCATE_AT) + "…" : speech, 5000);
256
257      // Mark assistant as in tutorial mode
258      assistant.setTutorialMode(true);
259    }
260
261    // Position the card and assistant after DOM update
262    requestAnimationFrame(() => {
263      this._positionCard(targetEl, step.position);
264      this._positionAssistant(targetEl, step.position);
265    });
266  }
267
268  // ── Card positioning ─────────────────────────────────────────────────────
269
270  _positionCard(targetEl, hint = "center") {
271    const card   = this._card;
272    const margin = 16;
273
274    if (!targetEl) {
275      card.style.top       = "50%";
276      card.style.left      = "50%";
277      card.style.transform = "translate(-50%, -50%)";
278      return;
279    }
280
281    card.style.transform = "";
282    const rect  = targetEl.getBoundingClientRect();
283    const cardW = card.offsetWidth  || 340;
284    const cardH = card.offsetHeight || 200;
285
286    let top, left;
287
288    if (hint === "top") {
289      top = rect.top - cardH - margin;
290      if (top < margin) top = rect.bottom + margin; // flip below if not enough room
291    } else if (hint === "bottom") {
292      top = rect.bottom + margin;
293    } else {
294      top = rect.top + rect.height / 2 - cardH / 2;
295    }
296
297    // Horizontally centre over the target, clamped to viewport
298    left = rect.left + rect.width / 2 - cardW / 2;
299    left = Math.max(margin, Math.min(left, window.innerWidth  - cardW - margin));
300    top  = Math.max(margin, Math.min(top,  window.innerHeight - cardH - margin));
301
302    card.style.top  = `${Math.round(top)}px`;
303    card.style.left = `${Math.round(left)}px`;
304  }
305
306  // ── Assistant positioning ────────────────────────────────────────────────
307
308  _positionAssistant(targetEl, hint = "center") {
309    const assistant = window.zimAssistant;
310    if (!assistant || !assistant.el) return;
311
312    const margin = 20;
313    const assistantEl = assistant.el;
314
315    if (!targetEl) {
316      // Center of screen - position assistant to the side of the card
317      assistantEl.style.position = "fixed";
318      assistantEl.style.bottom = "auto";
319      assistantEl.style.right = "auto";
320      assistantEl.style.top = "50%";
321      assistantEl.style.left = "calc(50% + 200px)"; // To the right of center card
322      assistantEl.style.transform = "translateY(-50%)";
323      return;
324    }
325
326    const rect = targetEl.getBoundingClientRect();
327    const assistantW = 120; // approximate width including bubble
328    const assistantH = 120; // approximate height including bubble
329
330    let top, left;
331
332    // Position assistant near the target but not overlapping the card
333    if (hint === "top") {
334      // Target is at top, card is above or below it
335      // Place assistant to the side of the target
336      top = rect.top + rect.height / 2 - assistantH / 2;
337      left = rect.right + margin;
338
339      // If not enough room on right, try left
340      if (left + assistantW > window.innerWidth - margin) {
341        left = rect.left - assistantW - margin;
342      }
343    } else if (hint === "bottom") {
344      top = rect.top + rect.height / 2 - assistantH / 2;
345      left = rect.right + margin;
346
347      if (left + assistantW > window.innerWidth - margin) {
348        left = rect.left - assistantW - margin;
349      }
350    } else {
351      // Center positioning - place to the side
352      top = rect.top + rect.height / 2 - assistantH / 2;
353      left = rect.right + margin;
354
355      if (left + assistantW > window.innerWidth - margin) {
356        left = rect.left - assistantW - margin;
357      }
358    }
359
360    // Clamp to viewport - use larger bottom margin to prevent agent from dropping too low
361    left = Math.max(margin, Math.min(left, window.innerWidth - assistantW - margin));
362    top = Math.max(margin, Math.min(top, window.innerHeight - assistantH - 100));
363
364    assistantEl.style.position = "fixed";
365    assistantEl.style.bottom = "auto";
366    assistantEl.style.right = "auto";
367    assistantEl.style.top = `${Math.round(top)}
367px`;
368    assistantEl.style.left = `${Math.round(left)}px`;
369    assistantEl.style.transform = "";
370    assistantEl.style.transition = "top 0.5s cubic-bezier(0.4, 0, 0.2, 1), left 0.5s cubic-bezier(0.4, 0, 0.2, 1)";
371  }
372
373  // ── Navigation ───────────────────────────────────────────────────────────
374
375  _go(idx) {
376    this._step = idx;
377    this._showStep(idx);
378  }
379
380  _finish() {
381    const assistant = window.zimAssistant;
382    if (assistant) assistant.say("Tutorial complete! Happy building!", 4000);
383    this.stop();
384  }
385}
386
387// ─── Singleton + exports ──────────────────────────────────────────────────────
388let _tutorial = null;
389
390/**
391 * Initialise the tutorial manager.
392 * Auto-starts for first-time visitors (after a short delay to let the
393 * assistant greeting finish first).
394 */
395export function initTutorial() {
396  if (_tutorial) return _tutorial;
397  _tutorial = new TutorialManager();
398
399  const done = (() => { try { return localStorage.getItem(TUTORIAL_KEY); } catch { return null; } })();
400  if (!done) {
401    // Let the assistant's greeting finish before beginning the tour
402    setTimeout(() => _tutorial.start(), TUTORIAL_AUTO_START_DELAY);
403  }
404
405  return _tutorial;
406}
407
408/** Manually (re-)start the tutorial — e.g. from a help button. */
409export function startTutorial() {
410  initTutorial(); // ensures _tutorial exists
411  _tutorial.start();
412}
413
414/** Start tutorial from a specific topic */
415export function startTutorialTopic(topic) {
416  initTutorial(); // ensures _tutorial exists
417  _tutorial.startFromTopic(topic);
418}
419
420// Listen for help topic requests from the assistant
421document.addEventListener("assistant:help-topic", (e) => {
422  const topic = e?.detail?.topic;
423  if (topic) {
424    startTutorialTopic(topic);
425  }
426});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.