/** * GuidedTour – Multilingual, versioned guided tour using Driver.js. * Shares language preference with HelpTooltip (localStorage key: onnx_explorer_help_lang). * Auto-start shows only NEW steps; button click shows ALL steps. * Requirements: 29.1–29.8 */ (function () { 'use strict'; // ─── Current tour version ───────────────────────────────────────────────── var CURRENT_VERSION = 3; // ─── Shared language config ─────────────────────────────────────────────── var LANG_STORAGE_KEY = 'onnx_explorer_help_lang'; var DEFAULT_LANG = 'en'; var SUPPORTED_LANGS = ['en', 'vi', 'ja']; // ─── UI strings per language ────────────────────────────────────────────── var UI_STRINGS = { en: { next: 'Next →', prev: '← Back', done: 'Done ✓', btnLabel: 'Guide' }, vi: { next: 'Tiếp theo →', prev: '← Quay lại', done: 'Hoàn thành ✓', btnLabel: 'Hướng dẫn' }, ja: { next: '次へ →', prev: '← 戻る', done: '完了 ✓', btnLabel: 'ガイド' } }; // ─── Tour steps (multilingual, organised by version) ────────────────────── var STEPS_V1 = [ { element: '#modelListContainer', popover: { title: { en: 'Model List', vi: 'Danh Sách Mô Hình', ja: 'モデル一覧' }, description: { en: 'Available ONNX models are listed here. Click a model to explore its details.', vi: 'Đây là danh sách các mô hình ONNX có sẵn. Click vào một mô hình để xem chi tiết.', ja: '利用可能なONNXモデルの一覧です。モデルをクリックして詳細を確認できます。' }, side: 'right', align: 'start' } }, { element: '#searchInput', popover: { title: { en: 'Search Models', vi: 'Tìm Kiếm Mô Hình', ja: 'モデル検索' }, description: { en: 'Type a model name to quickly filter the list.', vi: 'Nhập tên mô hình để tìm kiếm nhanh trong danh sách.', ja: 'モデル名を入力してリストをすばやくフィルタリングします。' }, side: 'bottom', align: 'start' } }, { element: '#uploadBtn', popover: { title: { en: 'Upload Model', vi: 'Tải Lên Mô Hình', ja: 'モデルをアップロード' }, description: { en: 'Click to upload an ONNX file from your computer.', vi: 'Click để tải lên tệp ONNX từ máy tính của bạn.', ja: 'クリックしてコンピュータからONNXファイルをアップロードします。' }, side: 'bottom', align: 'start' } }, { element: '#metadataContainer', popover: { title: { en: 'Metadata', vi: 'Siêu Dữ Liệu', ja: 'メタデータ' }, description: { en: 'General model info: producer name, version, opset and IR version.', vi: 'Thông tin chung về mô hình: tên nhà sản xuất, phiên bản, opset và IR version.', ja: 'モデルの一般情報:プロデューサー名、バージョン、opsetおよびIRバージョン。' }, side: 'left', align: 'start' } }, { element: '#inputOutputContainer', popover: { title: { en: 'Inputs & Outputs', vi: 'Đầu Vào & Đầu Ra', ja: '入力と出力' }, description: { en: 'Model input and output tensors with name, shape and data type.', vi: 'Các tensor đầu vào và đầu ra của mô hình với tên, shape và kiểu dữ liệu.', ja: 'モデルの入出力テンソル(名前、形状、データ型)を表示します。' }, side: 'left', align: 'start' } }, { element: '#initializerContainer', popover: { title: { en: 'Initializers', vi: 'Bộ Khởi Tạo', ja: '初期化子' }, description: { en: 'Constant tensors (weights, biases) embedded in the model.', vi: 'Danh sách các tensor hằng số (trọng số, bias) được nhúng trong mô hình.', ja: 'モデルに埋め込まれた定数テンソル(重み、バイアス)の一覧です。' }, side: 'top', align: 'start' } }, { element: '#graphContainer', popover: { title: { en: 'Model Graph', vi: 'Đồ Thị Mô Hình', ja: 'モデルグラフ' }, description: { en: 'Visualises the computation graph. Zoom, pan and click nodes to explore.', vi: 'Trực quan hóa đồ thị tính toán. Zoom, pan và click vào node để khám phá.', ja: '計算グラフを可視化します。ズーム、パン、ノードクリックで探索できます。' }, side: 'top', align: 'center' } }, { element: '#nodeDetailContainer', popover: { title: { en: 'Node Details', vi: 'Chi Tiết Node', ja: 'ノード詳細' }, description: { en: 'Click a node in the graph to see its full details here.', vi: 'Click vào một node trong đồ thị để xem thông tin chi tiết ở đây.', ja: 'グラフ内のノードをクリックすると、ここに詳細が表示されます。' }, side: 'left', align: 'start' } }, { element: '#exportBtn', popover: { title: { en: 'Export Info', vi: 'Xuất Thông Tin', ja: '情報をエクスポート' }, description: { en: 'Export all model information as a JSON file.', vi: 'Xuất toàn bộ thông tin mô hình dưới dạng tệp JSON.', ja: 'モデル情報をJSONファイルとしてエクスポートします。' }, side: 'bottom', align: 'end' } }, { element: '.help-icon', popover: { title: { en: 'Help Icons', vi: 'Icon Trợ Giúp', ja: 'ヘルプアイコン' }, description: { en: 'Click the (?) icon next to any section header for a multilingual explanation.', vi: 'Click vào icon (?) bên cạnh mỗi đầu mục để xem giải thích bằng nhiều ngôn ngữ.', ja: '各セクションヘッダーの(?)アイコンをクリックすると、多言語の説明が表示されます。' }, side: 'bottom', align: 'start' } } ]; var STEPS_V2 = [ { element: '.graph-search-container', popover: { title: { en: '🔍 Graph Node Search', vi: '🔍 Tìm Kiếm Node Trong Đồ Thị', ja: '🔍 グラフノード検索' }, description: { en: 'Search nodes by name or opType. Use ▲▼ to navigate results — the graph auto-zooms to each match.', vi: 'Tìm node theo tên hoặc opType. Dùng nút ▲▼ để duyệt kết quả, đồ thị tự động zoom đến node tìm thấy.', ja: '名前またはopTypeでノードを検索します。▲▼で結果を移動し、グラフが自動ズームします。' }, side: 'bottom', align: 'start' } }, { element: '#graphLayoutBtn', popover: { title: { en: '📐 Graph Layout', vi: '📐 Chuyển Đổi Layout Đồ Thị', ja: '📐 グラフレイアウト' }, description: { en: 'Switch between Top-Down, Left-Right, Circle or Force-Directed layouts. Your choice is saved.', vi: 'Chọn bố cục: Top-Down, Left-Right, Circle hoặc Force-Directed. Lựa chọn được lưu lại.', ja: 'Top-Down、Left-Right、Circle、Force-Directedレイアウトを切り替えます。選択は保存されます。' }, side: 'bottom', align: 'end' } }, { element: '#nodeGroupingBtn', popover: { title: { en: '📦 Node Grouping', vi: '📦 Gom Nhóm Node', ja: '📦 ノードグループ化' }, description: { en: 'Group nodes by operator type to simplify large graphs. Click a group to expand/collapse.', vi: 'Gom các node cùng opType thành nhóm để đơn giản hóa đồ thị lớn. Click nhóm để mở rộng/thu gọn.', ja: 'オペレータタイプでノードをグループ化し、大きなグラフを簡素化します。グループをクリックで展開/折りたたみ。' }, side: 'bottom', align: 'end' } }, { element: '#nodeDetailContainer', popover: { title: { en: '📝 Node Annotations', vi: '📝 Ghi Chú Node', ja: '📝 ノード注釈' }, description: { en: 'Right-click a node or use the "Annotate" button in the detail panel to add notes. Annotations are saved per model.', vi: 'Click phải vào node hoặc dùng nút "Annotate" trong panel chi tiết để thêm ghi chú. Ghi chú được lưu theo từng mô hình.', ja: 'ノードを右クリックするか、詳細パネルの「Annotate」ボタンで注釈を追加します。注釈はモデルごとに保存されます。' }, side: 'left', align: 'start' } }, { element: '#flopsEstimatorContainer', popover: { title: { en: '⚡ FLOPs Estimation', vi: '⚡ Ước Tính FLOPs', ja: '⚡ FLOPs推定' }, description: { en: 'Estimated computational cost (FLOPs) for the entire model, broken down by operator type.', vi: 'Ước tính chi phí tính toán (FLOPs) cho toàn bộ mô hình, phân tích theo từng loại toán tử.', ja: 'モデル全体の推定計算コスト(FLOPs)をオペレータタイプ別に表示します。' }, side: 'top', align: 'start' } }, { element: '#langSwitcherBtn', popover: { title: { en: '🌐 Language', vi: '🌐 Ngôn Ngữ', ja: '🌐 言語' }, description: { en: 'Switch the interface language between English, Vietnamese and Japanese. Applies to help tooltips and guided tour.', vi: 'Chuyển đổi ngôn ngữ giao diện giữa Tiếng Anh, Tiếng Việt và Tiếng Nhật. Áp dụng cho tooltip trợ giúp và hướng dẫn.', ja: 'インターフェース言語を英語、ベトナム語、日本語に切り替えます。ヘルプツールチップとガイドツアーに適用されます。' }, side: 'bottom', align: 'start' } } ]; var STEPS_V3 = [ { element: '#uploadBtn', popover: { title: { en: '📂 SafeTensors Support', vi: '📂 Hỗ Trợ SafeTensors', ja: '📂 SafeTensorsサポート' }, description: { en: 'You can now upload .safetensors files in addition to .onnx files. The viewer displays tensor list, metadata and summary.', vi: 'Bạn có thể tải lên tệp .safetensors ngoài tệp .onnx. Trình xem hiển thị danh sách tensor, metadata và tổng quan.', ja: '.onnxファイルに加えて.safetensorsファイルもアップロードできます。ビューアはテンソル一覧、メタデータ、サマリーを表示します。' }, side: 'bottom', align: 'start' } } ]; // ─── Version registry ───────────────────────────────────────────────────── var VERSION_MAP = { 1: STEPS_V1, 2: STEPS_V2, 3: STEPS_V3 }; // ─── Language helpers ───────────────────────────────────────────────────── function _getLang() { try { // Share language with HelpTooltip var lang = localStorage.getItem(LANG_STORAGE_KEY); if (lang && SUPPORTED_LANGS.indexOf(lang) !== -1) return lang; } catch (_) { /* ignore */ } return DEFAULT_LANG; } /** Resolve a multilingual value: if it's an object with lang keys, pick current lang. */ function _t(val) { if (val && typeof val === 'object' && !Array.isArray(val)) { var lang = _getLang(); return val[lang] || val[DEFAULT_LANG] || val.en || ''; } return val || ''; } /** Convert multilingual step definitions into Driver.js-ready steps for current language. */ function _localizeSteps(steps) { return steps.map(function (step) { var pop = step.popover || {}; return { element: step.element, popover: { title: _t(pop.title), description: _t(pop.description), side: pop.side, align: pop.align } }; }); } // ─── Step collection helpers ────────────────────────────────────────────── function _getAllSteps() { var all = []; for (var v = 1; v <= CURRENT_VERSION; v++) { if (VERSION_MAP[v]) all = all.concat(VERSION_MAP[v]); } return all; } function _getNewSteps(completedVersion) { var steps = []; for (var v = completedVersion + 1; v <= CURRENT_VERSION; v++) { if (VERSION_MAP[v]) steps = steps.concat(VERSION_MAP[v]); } return steps; } // ─── GuidedTour class ───────────────────────────────────────────────────── function GuidedTour(options) { options = options || {}; this._storageKey = options.storageKey || 'onnx_explorer_tour_version'; this._driverObj = null; this._tourBtn = null; } GuidedTour.prototype.init = function () { if (!window.driver) { console.warn('[GuidedTour] Driver.js not loaded – tour unavailable.'); return; } this._createTourButton(); var completed = this._getCompletedVersion(); if (completed < CURRENT_VERSION) { var self = this; setTimeout(function () { self._startNewStepsTour(); }, 1000); } }; GuidedTour.prototype._createTourButton = function () { var copyLinkBtn = document.getElementById('copyLinkBtn'); if (!copyLinkBtn) return; var btn = document.createElement('button'); btn.id = 'tourBtn'; btn.className = 'btn btn-outline-light btn-sm me-2'; btn.title = _t({ en: 'User guide', vi: 'Hướng dẫn sử dụng', ja: 'ユーザーガイド' }); btn.innerHTML = ' ' + (UI_STRINGS[_getLang()] || UI_STRINGS[DEFAULT_LANG]).btnLabel; var self = this; btn.addEventListener('click', function () { self.startFullTour(); }); copyLinkBtn.parentNode.insertBefore(btn, copyLinkBtn); this._tourBtn = btn; }; /** Full tour (all versions) — triggered by button click. */ GuidedTour.prototype.startFullTour = function () { this._runTour(_getAllSteps()); }; /** Only new (unseen) steps — used for auto-start. */ GuidedTour.prototype._startNewStepsTour = function () { var newSteps = _getNewSteps(this._getCompletedVersion()); if (newSteps.length > 0) this._runTour(newSteps); }; /** Backward-compatible alias. */ GuidedTour.prototype.startTour = function () { this.startFullTour(); }; GuidedTour.prototype._runTour = function (steps) { if (!window.driver || !steps || steps.length === 0) return; var localized = _localizeSteps(steps); // Filter out steps whose target element doesn't exist var valid = localized.filter(function (s) { return !s.element || document.querySelector(s.element); }); if (valid.length === 0) return; var self = this; var lang = _getLang(); var ui = UI_STRINGS[lang] || UI_STRINGS[DEFAULT_LANG]; var driverFn = window.driver.js.driver; this._driverObj = driverFn({ showProgress: true, animate: true, overlayColor: 'rgba(0, 0, 0, 0.6)', stagePadding: 8, popoverClass: 'guided-tour-popover', nextBtnText: ui.next, prevBtnText: ui.prev, doneBtnText: ui.done, onDestroyStarted: function () { self.markCompleted(); if (self._driverObj) self._driverObj.destroy(); }, steps: valid }); this._driverObj.drive(); }; // ─── Version / persistence ──────────────────────────────────────────────── GuidedTour.prototype.markCompleted = function () { try { localStorage.setItem(this._storageKey, String(CURRENT_VERSION)); } catch (_) {} }; GuidedTour.prototype._getCompletedVersion = function () { try { // Migrate old boolean key var oldKey = 'onnx_explorer_tour_completed'; var oldVal = localStorage.getItem(oldKey); if (oldVal === 'true') { localStorage.removeItem(oldKey); localStorage.setItem(this._storageKey, '1'); return 1; } var num = parseInt(localStorage.getItem(this._storageKey), 10); return isNaN(num) ? 0 : num; } catch (_) { return 0; } }; GuidedTour.prototype.isCompleted = function () { return this._getCompletedVersion() >= CURRENT_VERSION; }; GuidedTour.prototype.reset = function () { try { localStorage.removeItem(this._storageKey); localStorage.removeItem('onnx_explorer_tour_completed'); } catch (_) {} }; GuidedTour.prototype.destroy = function () { if (this._driverObj) { this._driverObj.destroy(); this._driverObj = null; } if (this._tourBtn && this._tourBtn.parentNode) { this._tourBtn.parentNode.removeChild(this._tourBtn); this._tourBtn = null; } }; window.GuidedTour = GuidedTour; })();