diff --git a/src/extensionsIntegrated/Phoenix/phoenix-tour.js b/src/extensionsIntegrated/Phoenix/phoenix-tour.js index 7eee1aadd8..33f8241764 100644 --- a/src/extensionsIntegrated/Phoenix/phoenix-tour.js +++ b/src/extensionsIntegrated/Phoenix/phoenix-tour.js @@ -33,6 +33,7 @@ define(function (require, exports, module) { StringUtils = require("utils/StringUtils"), Metrics = require("utils/Metrics"), SidebarView = require("project/SidebarView"), + SidebarTabs = require("view/SidebarTabs"), ProjectManager = require("project/ProjectManager"), EditorManager = require("editor/EditorManager"), CommandManager = require("command/CommandManager"), @@ -76,6 +77,21 @@ define(function (require, exports, module) { let _rafId = null; let _timers = []; + // Per-step: tracks a click on the highlighted target while the + // overlay is showing, fires a metric, then detaches. Cleared by + // _detachStepClickMetric on step transitions and teardown. + let _activeStepClickHandler = null; + let _activeStepClickTarget = null; + + // Step 2: when the step starts we briefly switch the sidebar to the + // AI tab as an automatic peek (2s) and revert to whatever the user + // was on. _peekPrevTab is non-null only while a peek is in flight so + // teardown can revert cleanly if the tour ends mid-peek. + let _step2PeekTimer = null; + let _step2PeekPrevTab = null; + const STEP2_PEEK_HOLD_MS = 2000; + const SIDEBAR_AI_TAB_ID = "ai"; + function _markComplete() { _state.version = CURRENT_TOUR_VERSION; _saveState(_state); @@ -92,8 +108,74 @@ define(function (require, exports, module) { } } + /** + * Attach a one-shot click listener to `$target` that fires a "stepN_clicked" + * metric. Captures real user clicks during the overlay session — not the + * synthetic class toggles the demos do. Replaces any previously attached + * step handler so we never double-count across step transitions. + */ + function _attachStepClickMetric(stepNum, $target) { + _detachStepClickMetric(); + if (!$target || !$target.length || !$target[0]) { + return; + } + const targetEl = $target[0]; + const handler = function () { + Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step" + stepNum + "_clicked"); + _detachStepClickMetric(); + }; + targetEl.addEventListener("click", handler, true); + _activeStepClickTarget = targetEl; + _activeStepClickHandler = handler; + } + + function _detachStepClickMetric() { + if (_activeStepClickTarget && _activeStepClickHandler) { + _activeStepClickTarget.removeEventListener("click", _activeStepClickHandler, true); + } + _activeStepClickTarget = null; + _activeStepClickHandler = null; + } + + /** + * Step 2 only: automatically switch the sidebar to the AI tab for a + * couple of seconds so the user sees what's behind the tab, then + * revert. No-op if the user is already on the AI tab. + */ + function _runStep2AIPeek() { + _cancelStep2AIPeek(); + const current = SidebarTabs.getActiveTab && SidebarTabs.getActiveTab(); + if (current === SIDEBAR_AI_TAB_ID) { + return; + } + _step2PeekPrevTab = current; + SidebarTabs.setActiveTab(SIDEBAR_AI_TAB_ID); + _step2PeekTimer = setTimeout(function () { + if (_step2PeekPrevTab) { + SidebarTabs.setActiveTab(_step2PeekPrevTab); + } + _step2PeekPrevTab = null; + _step2PeekTimer = null; + }, STEP2_PEEK_HOLD_MS); + } + + function _cancelStep2AIPeek() { + if (_step2PeekTimer) { + clearTimeout(_step2PeekTimer); + _step2PeekTimer = null; + } + // If we tore down or transitioned mid-peek, restore the previous + // tab so the sidebar doesn't get stranded on AI. + if (_step2PeekPrevTab) { + SidebarTabs.setActiveTab(_step2PeekPrevTab); + _step2PeekPrevTab = null; + } + } + function _teardown() { _clearTimers(); + _detachStepClickMetric(); + _cancelStep2AIPeek(); if ($overlay) { $overlay.remove(); $overlay = null; @@ -213,6 +295,7 @@ define(function (require, exports, module) { _trackTarget($btn, "right"); _setStep(1); Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step1"); + _attachStepClickMetric(1, $btn); // Single, stable message for the entire step. The visible toggle of // design mode does the explaining; rotating text under a 2-second // demo is too quick to read. @@ -248,6 +331,9 @@ define(function (require, exports, module) { } function _runStep2() { + // Each step transition cancels the previous step's instrumentation. + _detachStepClickMetric(); + _cancelStep2AIPeek(); _ensureSidebarVisible(); const $tab = $('.sidebar-tab[data-tab-id="ai"]'); if (!$tab.length) { @@ -269,11 +355,17 @@ define(function (require, exports, module) { } } ]); + _attachStepClickMetric(2, $tab); + // Auto-peek the AI panel for a couple of seconds so the user gets + // a glance at its contents, then revert. + _runStep2AIPeek(); // Intentionally do NOT advance on a real click of the target — the // user needs time to read the prompt; only the Next button advances. } function _runStep3() { + _detachStepClickMetric(); + _cancelStep2AIPeek(); _ensureSidebarVisible(); const $newBtn = $("#newProject"); if (!$newBtn.length) { @@ -286,6 +378,7 @@ define(function (require, exports, module) { _trackTarget($newBtn, "right"); _setStep(3); Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step3"); + _attachStepClickMetric(3, $newBtn); _setText(Strings.PHOENIX_TOUR_NEW_PROJECT); _setActions([ { @@ -354,6 +447,8 @@ define(function (require, exports, module) { } async function _runStep4() { + _detachStepClickMetric(); + _cancelStep2AIPeek(); _ensureSidebarVisible(); try { await _ensureLivePreviewReady(); @@ -375,6 +470,7 @@ define(function (require, exports, module) { _trackTarget($btn, "left"); _setStep(4); Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step4"); + _attachStepClickMetric(4, $btn); _setText(Strings.PHOENIX_TOUR_EDIT_MODE); _setActions([ { diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 740eb42db9..976b08d025 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2216,6 +2216,7 @@ define({ "AI_CHAT_TOOL_RESIZE_PREVIEW": "Resize preview", "AI_LIVE_PREVIEW_BANNER_TEXT": "AI is inspecting the live preview", "AI_LIVE_PREVIEW_BANNER_RESIZE": "AI resized preview to {0}", + "AI_LIVE_PREVIEW_BANNER_DISMISS_TOOLTIP": "Click to dismiss", "AI_CHAT_TOOL_CONTROL_EDITOR": "Editor", "AI_CHAT_TOOL_TASKS": "Tasks", "AI_CHAT_TOOL_TASKS_SUMMARY": "{0} of {1} tasks done", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index af7b30112f..5b1710fe35 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -419,6 +419,48 @@ } } + .ai-onboarding-prompt-images { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 8px 12px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + background: @bc-ai-input-bg; + + .ai-image-thumb { + position: relative; + + img { + display: block; + max-width: 64px; + max-height: 48px; + object-fit: cover; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.12); + cursor: pointer; + } + + .ai-image-remove { + position: absolute; + top: -6px; + right: -6px; + width: 18px; + height: 18px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(255, 255, 255, 0.3); + color: #fff; + font-size: 11px; + line-height: 1; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + } + } + .ai-onboarding-prompt-actions { display: flex; justify-content: flex-end; @@ -602,6 +644,10 @@ } } +/* Fullscreen video overlay opened from the AI panel intro thumbnail. + Position fixed so it covers the entire app viewport (not just the + AI panel column). The inner player is sized to use most of the + screen while preserving 16:9 letterboxing inside the dark backdrop. */ /* ── Assistant message — markdown content ───────────────────────────── */ .ai-msg-assistant { .ai-msg-content { diff --git a/src/styles/VideoPlayer.less b/src/styles/VideoPlayer.less new file mode 100644 index 0000000000..a1fe9686f2 --- /dev/null +++ b/src/styles/VideoPlayer.less @@ -0,0 +1,87 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* Styles for view/VideoPlayer.js — both the inline createPlayer wrapper + * and the renderFullScreenPlayer overlay (genie-style expand-from-source + * animation). The fullscreen overlay's player-expand animation is + * JS-driven (FLIP transform) so it can target the actual source rect at + * click time; only the backdrop fade is keyframe-driven. */ + +@keyframes phx-video-fs-backdrop-in { + from { background-color: rgba(0, 0, 0, 0); } + to { background-color: rgba(0, 0, 0, 0.88); } +} + +.phx-video-fullscreen-overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.88); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 24px; + box-sizing: border-box; + animation: phx-video-fs-backdrop-in 220ms ease-out; + + .phx-video-fullscreen-player { + cursor: default; + width: min(90vw, calc((90vh - 48px) * 16 / 9)); + max-width: 90vw; + max-height: 90vh; + aspect-ratio: 16 / 9; + background: #000; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 6px 32px rgba(0, 0, 0, 0.6); + transform-origin: center; + will-change: transform, opacity; + + video { + display: block; + width: 100%; + height: 100%; + background: #000; + } + } + + .phx-video-fullscreen-close { + position: absolute; + top: 14px; + right: 14px; + width: 36px; + height: 36px; + border: 0; + border-radius: 50%; + background: rgba(0, 0, 0, 0.55); + color: #fff; + font-size: 16px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.15s ease, transform 0.15s ease; + + &:hover { + background: rgba(0, 0, 0, 0.8); + transform: scale(1.05); + } + } +} diff --git a/src/styles/brackets.less b/src/styles/brackets.less index c96be89166..78891cc4c8 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -54,6 +54,7 @@ @import "Extn-Terminal.less"; @import "UserProfile.less"; @import "phoenix-pro.less"; +@import "VideoPlayer.less"; /* Overall layout */ diff --git a/src/view/VideoPlayer.js b/src/view/VideoPlayer.js new file mode 100644 index 0000000000..1d2f9cca48 --- /dev/null +++ b/src/view/VideoPlayer.js @@ -0,0 +1,235 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +// @INCLUDE_IN_API_DOCS + +/** + * Tiny shared HTML5 `