mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Refine What's New update gating
Show the What's New modal once per newer version by default, add a browser-local permanent opt-out checkbox, and expose the modal through the builtin plugin Open action. Keep the legacy modal path as a compatibility redirect and update the local DOX/tests for the new contract.
This commit is contained in:
parent
90abe05d2d
commit
82bb0b9292
6 changed files with 481 additions and 299 deletions
|
|
@ -2,31 +2,37 @@
|
|||
|
||||
## Purpose
|
||||
|
||||
- Own the built-in version-gated "What's New" modal for showcasing new Agent Zero features after updates.
|
||||
- Own the built-in version-gated "What's New" modal for showcasing Agent Zero features after updates.
|
||||
|
||||
## Ownership
|
||||
|
||||
- `plugin.yaml` owns metadata and always-enabled status.
|
||||
- `webui/` owns the modal markup, Alpine store, copy, and showcase media assets.
|
||||
- `extensions/webui/initFw_end/` owns the startup trigger that opens the modal when the installed version is newer than the locally seen version.
|
||||
- `webui/main.html` owns the canonical modal opened by startup and the Builtin Plugins `Open` button.
|
||||
- `webui/whats-new.html` is a compatibility redirect to `webui/main.html`.
|
||||
- `webui/` owns the Alpine store, copy, and showcase media assets.
|
||||
- `extensions/webui/initFw_end/` owns the startup trigger that opens the modal once per newer installed version unless the user has permanently opted out.
|
||||
|
||||
## Local Contracts
|
||||
|
||||
- Do not add a "Don't show this again" control; dismissal records the current installed version as seen.
|
||||
- Closing, Skip, or Done records only the current installed version as seen.
|
||||
- Future updates should auto-open the modal again unless the user checks the modal's permanent opt-out checkbox.
|
||||
- The permanent opt-out is stored in browser-local state under `a0_whats_new_never_show`.
|
||||
- Honor the legacy `a0_whats_new_seen_version` browser-local marker as the last seen version.
|
||||
- Keep the modal copy concise, left-aligned, and paired with feature media.
|
||||
- Keep modal actions in the pinned footer using the shared Agent Zero button classes.
|
||||
- Store the seen-version marker in browser-local state only; do not persist this under `usr/`.
|
||||
- Store seen-version and opt-out markers in browser-local state only; do not persist this under `usr/`.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
- Add showcase assets under `webui/assets/` and reference them through `/plugins/_whats_new/webui/assets/...`.
|
||||
- Keep the startup extension idempotent and tolerant of missing or non-comparable version labels.
|
||||
- Prefer release-line comparisons over development commit-distance comparisons so local development builds do not reopen the modal on every commit.
|
||||
- Preserve `webui/main.html` so the plugin list exposes the standard `Open` action.
|
||||
|
||||
## Verification
|
||||
|
||||
- Run `pytest tests/test_whats_new_static.py` after changing the modal, trigger, or assets.
|
||||
- Smoke-test startup display, slide navigation, dismissal, and same-version reload behavior in the WebUI.
|
||||
- Smoke-test startup display, slide navigation, dismissal, same-version reload behavior, newer-version display behavior, opt-out behavior, and manual Builtin Plugins `Open` behavior in the WebUI.
|
||||
|
||||
## Child DOX Index
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,37 @@
|
|||
import { getModalStack, isModalOpen, openModal } from "/js/modals.js";
|
||||
|
||||
const MODAL_PATH = "/plugins/_whats_new/webui/whats-new.html";
|
||||
const STORAGE_KEY = "a0_whats_new_seen_version";
|
||||
const MODAL_PATH = "/plugins/_whats_new/webui/main.html";
|
||||
const LEGACY_MODAL_PATH = "/plugins/_whats_new/webui/whats-new.html";
|
||||
const SEEN_VERSION_STORAGE_KEY = "a0_whats_new_seen_version";
|
||||
const NEVER_SHOW_STORAGE_KEY = "a0_whats_new_never_show";
|
||||
const INTERMEDIATE_ONCE_STORAGE_KEY = "a0_whats_new_seen_once";
|
||||
const STARTUP_DELAY_MS = 1200;
|
||||
const RETRY_DELAY_MS = 900;
|
||||
const MAX_BUSY_RETRIES = 20;
|
||||
|
||||
let initialized = false;
|
||||
let busyRetries = 0;
|
||||
let openedForVersion = null;
|
||||
|
||||
function cleanPath(path = "") {
|
||||
return String(path || "").replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
function storageValue(key) {
|
||||
try {
|
||||
return globalThis.localStorage?.getItem(key) || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function storageSet(key, value) {
|
||||
try {
|
||||
globalThis.localStorage?.setItem(key, value);
|
||||
} catch {
|
||||
// localStorage may be unavailable in private or locked-down browser modes.
|
||||
}
|
||||
}
|
||||
|
||||
function parseVersion(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw || raw.toLowerCase() === "unknown") return null;
|
||||
|
|
@ -31,6 +49,16 @@ function parseVersion(value) {
|
|||
};
|
||||
}
|
||||
|
||||
function parseStoredVersion(rawValue) {
|
||||
if (!rawValue) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(rawValue);
|
||||
return parseVersion(parsed?.version || parsed?.raw || rawValue);
|
||||
} catch {
|
||||
return parseVersion(rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
function compareVersions(left, right) {
|
||||
if (!left || !right) return null;
|
||||
for (let index = 0; index < left.parts.length; index += 1) {
|
||||
|
|
@ -45,23 +73,43 @@ function currentVersion() {
|
|||
return parseVersion(globalThis.gitinfo?.version || "");
|
||||
}
|
||||
|
||||
function storedSeenVersion() {
|
||||
try {
|
||||
const rawValue = globalThis.localStorage?.getItem(STORAGE_KEY) || "";
|
||||
if (!rawValue) return null;
|
||||
function markVersionSeen(version = currentVersion()) {
|
||||
if (!version) return;
|
||||
storageSet(
|
||||
SEEN_VERSION_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
version: version.raw,
|
||||
seenAt: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawValue);
|
||||
return parseVersion(parsed?.version || parsed?.raw || rawValue);
|
||||
} catch {
|
||||
return parseVersion(rawValue);
|
||||
}
|
||||
function storedSeenVersion() {
|
||||
const stored = parseStoredVersion(storageValue(SEEN_VERSION_STORAGE_KEY));
|
||||
if (stored) return stored;
|
||||
|
||||
const intermediate = parseStoredVersion(storageValue(INTERMEDIATE_ONCE_STORAGE_KEY));
|
||||
if (!intermediate) return null;
|
||||
|
||||
markVersionSeen(intermediate);
|
||||
return intermediate;
|
||||
}
|
||||
|
||||
function shouldNeverShow() {
|
||||
const value = storageValue(NEVER_SHOW_STORAGE_KEY);
|
||||
if (!value) return false;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed && typeof parsed === "object") return parsed.enabled !== false;
|
||||
return Boolean(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
return !["0", "false", "no", "off"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
function shouldShowWhatsNew(version) {
|
||||
function shouldShowWhatsNew(version = currentVersion()) {
|
||||
if (shouldNeverShow()) return false;
|
||||
if (!version) return false;
|
||||
|
||||
const seen = storedSeenVersion();
|
||||
|
|
@ -71,25 +119,19 @@ function shouldShowWhatsNew(version) {
|
|||
return comparison !== null && comparison > 0;
|
||||
}
|
||||
|
||||
function markVersionSeen(version = currentVersion()) {
|
||||
if (!version) return;
|
||||
try {
|
||||
globalThis.localStorage?.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
version: version.raw,
|
||||
seenAt: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// localStorage may be unavailable in private or locked-down browser modes.
|
||||
}
|
||||
}
|
||||
|
||||
function anotherModalIsOpen() {
|
||||
return getModalStack().length > 0 || Boolean(document.querySelector(".modal.show"));
|
||||
}
|
||||
|
||||
function isWhatsNewPath(path = "") {
|
||||
const cleaned = cleanPath(path);
|
||||
return [MODAL_PATH, LEGACY_MODAL_PATH].some((candidate) => cleanPath(candidate) === cleaned);
|
||||
}
|
||||
|
||||
function isWhatsNewOpen() {
|
||||
return isModalOpen(MODAL_PATH) || isModalOpen(LEGACY_MODAL_PATH);
|
||||
}
|
||||
|
||||
function scheduleRetry() {
|
||||
if (busyRetries >= MAX_BUSY_RETRIES) return;
|
||||
busyRetries += 1;
|
||||
|
|
@ -102,21 +144,19 @@ function maybeOpenWhatsNew() {
|
|||
const version = currentVersion();
|
||||
if (!shouldShowWhatsNew(version)) return;
|
||||
|
||||
if (isModalOpen(MODAL_PATH)) return;
|
||||
if (isWhatsNewOpen()) return;
|
||||
if (anotherModalIsOpen()) {
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
openedForVersion = version;
|
||||
void openModal(MODAL_PATH);
|
||||
}
|
||||
|
||||
function handleModalClosed(event) {
|
||||
const closedPath = event?.detail?.modalPath || "";
|
||||
if (cleanPath(closedPath) === cleanPath(MODAL_PATH)) {
|
||||
markVersionSeen(openedForVersion || currentVersion());
|
||||
openedForVersion = null;
|
||||
if (isWhatsNewPath(closedPath)) {
|
||||
markVersionSeen();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
308
plugins/_whats_new/webui/main.html
Normal file
308
plugins/_whats_new/webui/main.html
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<html class="whats-new-modal">
|
||||
<head>
|
||||
<title>What's New in Agent Zero</title>
|
||||
<script type="module">
|
||||
import { store } from "/plugins/_whats_new/webui/whats-new-store.js";
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div x-data>
|
||||
<template x-if="$store.whatsNew">
|
||||
<div class="whats-new-shell" x-init="$store.whatsNew.onOpen()" x-destroy="$store.whatsNew.cleanup()">
|
||||
<section class="whats-new-media-panel" :aria-label="$store.whatsNew.currentSlide.mediaLabel">
|
||||
<template x-if="$store.whatsNew.currentSlide.mediaType === 'video'">
|
||||
<video
|
||||
class="whats-new-media"
|
||||
:src="$store.whatsNew.currentSlide.media"
|
||||
:aria-label="$store.whatsNew.currentSlide.mediaLabel"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
</template>
|
||||
<template x-if="$store.whatsNew.currentSlide.mediaType === 'image'">
|
||||
<img
|
||||
class="whats-new-media"
|
||||
:src="$store.whatsNew.currentSlide.media"
|
||||
:alt="$store.whatsNew.currentSlide.mediaLabel"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section class="whats-new-copy">
|
||||
<div class="whats-new-eyebrow" x-text="$store.whatsNew.currentSlide.eyebrow"></div>
|
||||
<h3 x-text="$store.whatsNew.currentSlide.title"></h3>
|
||||
<p class="whats-new-summary" x-text="$store.whatsNew.currentSlide.summary"></p>
|
||||
<ul class="whats-new-bullets">
|
||||
<template x-for="item in $store.whatsNew.currentSlide.bullets" :key="item">
|
||||
<li x-text="item"></li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="modal-footer whats-new-footer" data-modal-footer>
|
||||
<div class="whats-new-footer-left">
|
||||
<div class="whats-new-footer-progress">
|
||||
<span class="whats-new-progress-label" x-text="$store.whatsNew.progressLabel()"></span>
|
||||
<div class="whats-new-progress-dots" role="tablist" aria-label="What's New slides">
|
||||
<template x-for="(slide, index) in $store.whatsNew.slides" :key="slide.id">
|
||||
<button
|
||||
type="button"
|
||||
class="whats-new-dot"
|
||||
role="tab"
|
||||
:class="{ 'active': index === $store.whatsNew.currentIndex }"
|
||||
:aria-label="$store.whatsNew.dotLabel(index)"
|
||||
:aria-selected="index === $store.whatsNew.currentIndex ? 'true' : 'false'"
|
||||
@click="$store.whatsNew.goTo(index)"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<label class="whats-new-never-show">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="$store.whatsNew.neverShowAgain"
|
||||
@change="$store.whatsNew.setNeverShowAgain($event.target.checked)"
|
||||
/>
|
||||
<span>Don't show automatically again</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="whats-new-footer-actions">
|
||||
<button type="button" class="btn btn-cancel" @click="$store.whatsNew.skip()">Skip</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-field"
|
||||
x-show="!$store.whatsNew.isFirst()"
|
||||
@click="$store.whatsNew.previous()"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button type="button" class="btn btn-ok" @click="$store.whatsNew.next()">
|
||||
<span x-text="$store.whatsNew.isLast() ? 'Done' : 'Next'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-inner.whats-new-modal {
|
||||
width: min(92vw, 760px);
|
||||
max-height: min(88vh, 820px);
|
||||
}
|
||||
|
||||
.whats-new-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.whats-new-media-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1476 / 842;
|
||||
min-height: 180px;
|
||||
max-height: min(44vh, 360px);
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
|
||||
border-radius: 7px;
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, color-mix(in srgb, #4248f1 18%, transparent), transparent 34%),
|
||||
color-mix(in srgb, var(--color-panel) 78%, #000 22%);
|
||||
}
|
||||
|
||||
.whats-new-media {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: color-mix(in srgb, var(--color-panel) 82%, #000 18%);
|
||||
}
|
||||
|
||||
.whats-new-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.whats-new-eyebrow {
|
||||
color: color-mix(in srgb, #2196f3 86%, var(--color-text));
|
||||
font-size: 0.74rem;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.whats-new-copy h3 {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: clamp(1.35rem, 2.5vw, 1.8rem);
|
||||
font-weight: 760;
|
||||
line-height: 1.12;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.whats-new-summary {
|
||||
max-width: 62ch;
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.whats-new-bullets {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin: 0.25rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.whats-new-bullets li {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.whats-new-bullets li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.58em;
|
||||
width: 0.42rem;
|
||||
height: 0.42rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, #4248f1 72%, #2196f3 28%);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, #4248f1 18%, transparent);
|
||||
}
|
||||
|
||||
.whats-new-footer {
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.whats-new-footer-left,
|
||||
.whats-new-footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.whats-new-footer-left {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.whats-new-footer-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.whats-new-progress-label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.whats-new-progress-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.whats-new-dot {
|
||||
width: 0.48rem;
|
||||
height: 0.48rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-text-muted) 45%, transparent);
|
||||
cursor: pointer;
|
||||
transition: transform 0.16s ease, background-color 0.16s ease, width 0.16s ease;
|
||||
}
|
||||
|
||||
.whats-new-dot.active {
|
||||
width: 1.2rem;
|
||||
background: color-mix(in srgb, #4248f1 74%, #2196f3 26%);
|
||||
}
|
||||
|
||||
.whats-new-dot:hover {
|
||||
transform: translateY(-1px);
|
||||
background: color-mix(in srgb, #2196f3 72%, var(--color-text));
|
||||
}
|
||||
|
||||
.whats-new-never-show {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.whats-new-never-show input {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
margin: 0;
|
||||
flex: 0 0 auto;
|
||||
accent-color: #4248f1;
|
||||
}
|
||||
|
||||
.whats-new-never-show span {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.modal-inner.whats-new-modal {
|
||||
width: min(94vw, 760px);
|
||||
}
|
||||
|
||||
.whats-new-media-panel {
|
||||
min-height: 150px;
|
||||
max-height: 32vh;
|
||||
}
|
||||
|
||||
.whats-new-footer,
|
||||
.whats-new-footer-left,
|
||||
.whats-new-footer-actions {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.whats-new-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.whats-new-footer-left {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.whats-new-footer-progress {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.whats-new-footer-actions {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -2,6 +2,7 @@ import { createStore } from "/js/AlpineStore.js";
|
|||
import { closeModal } from "/js/modals.js";
|
||||
|
||||
const ASSET_BASE = "/plugins/_whats_new/webui/assets";
|
||||
const NEVER_SHOW_STORAGE_KEY = "a0_whats_new_never_show";
|
||||
|
||||
const slides = [
|
||||
{
|
||||
|
|
@ -54,15 +55,58 @@ const slides = [
|
|||
},
|
||||
];
|
||||
|
||||
function storageValue(key) {
|
||||
try {
|
||||
return globalThis.localStorage?.getItem(key) || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function isNeverShowEnabled() {
|
||||
const value = storageValue(NEVER_SHOW_STORAGE_KEY);
|
||||
if (!value) return false;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed && typeof parsed === "object") return parsed.enabled !== false;
|
||||
return Boolean(parsed);
|
||||
} catch {
|
||||
return !["0", "false", "no", "off"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
function persistNeverShowPreference(enabled) {
|
||||
try {
|
||||
if (enabled) {
|
||||
globalThis.localStorage?.setItem(
|
||||
NEVER_SHOW_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
enabled: true,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
globalThis.localStorage?.removeItem(NEVER_SHOW_STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// localStorage may be unavailable in private or locked-down browser modes.
|
||||
}
|
||||
}
|
||||
|
||||
export const store = createStore("whatsNew", {
|
||||
slides,
|
||||
currentIndex: 0,
|
||||
neverShowAgain: false,
|
||||
|
||||
onOpen() {
|
||||
this.currentIndex = 0;
|
||||
this.neverShowAgain = isNeverShowEnabled();
|
||||
},
|
||||
|
||||
cleanup() {},
|
||||
cleanup() {
|
||||
persistNeverShowPreference(this.neverShowAgain);
|
||||
},
|
||||
|
||||
get currentSlide() {
|
||||
return this.slides[this.currentIndex] || this.slides[0];
|
||||
|
|
@ -96,6 +140,11 @@ export const store = createStore("whatsNew", {
|
|||
if (!this.isFirst()) this.currentIndex -= 1;
|
||||
},
|
||||
|
||||
setNeverShowAgain(value) {
|
||||
this.neverShowAgain = Boolean(value);
|
||||
persistNeverShowPreference(this.neverShowAgain);
|
||||
},
|
||||
|
||||
next() {
|
||||
if (this.isLast()) {
|
||||
this.finish();
|
||||
|
|
|
|||
|
|
@ -1,259 +1,20 @@
|
|||
<html class="whats-new-modal">
|
||||
<html>
|
||||
<head>
|
||||
<title>What's New in Agent Zero</title>
|
||||
<script type="module">
|
||||
import { store } from "/plugins/_whats_new/webui/whats-new-store.js";
|
||||
import { closeModal, openModal } from "/js/modals.js";
|
||||
|
||||
const legacyPath = "/plugins/_whats_new/webui/whats-new.html";
|
||||
const mainPath = "/plugins/_whats_new/webui/main.html";
|
||||
|
||||
queueMicrotask(async () => {
|
||||
await closeModal(legacyPath);
|
||||
await openModal(mainPath);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div x-data>
|
||||
<template x-if="$store.whatsNew">
|
||||
<div class="whats-new-shell" x-init="$store.whatsNew.onOpen()" x-destroy="$store.whatsNew.cleanup()">
|
||||
<section class="whats-new-media-panel" :aria-label="$store.whatsNew.currentSlide.mediaLabel">
|
||||
<template x-if="$store.whatsNew.currentSlide.mediaType === 'video'">
|
||||
<video
|
||||
class="whats-new-media"
|
||||
:src="$store.whatsNew.currentSlide.media"
|
||||
:aria-label="$store.whatsNew.currentSlide.mediaLabel"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
</template>
|
||||
<template x-if="$store.whatsNew.currentSlide.mediaType === 'image'">
|
||||
<img
|
||||
class="whats-new-media"
|
||||
:src="$store.whatsNew.currentSlide.media"
|
||||
:alt="$store.whatsNew.currentSlide.mediaLabel"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section class="whats-new-copy">
|
||||
<div class="whats-new-eyebrow" x-text="$store.whatsNew.currentSlide.eyebrow"></div>
|
||||
<h3 x-text="$store.whatsNew.currentSlide.title"></h3>
|
||||
<p class="whats-new-summary" x-text="$store.whatsNew.currentSlide.summary"></p>
|
||||
<ul class="whats-new-bullets">
|
||||
<template x-for="item in $store.whatsNew.currentSlide.bullets" :key="item">
|
||||
<li x-text="item"></li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="modal-footer whats-new-footer" data-modal-footer>
|
||||
<div class="whats-new-footer-left">
|
||||
<span class="whats-new-progress-label" x-text="$store.whatsNew.progressLabel()"></span>
|
||||
<div class="whats-new-progress-dots" role="tablist" aria-label="What's New slides">
|
||||
<template x-for="(slide, index) in $store.whatsNew.slides" :key="slide.id">
|
||||
<button
|
||||
type="button"
|
||||
class="whats-new-dot"
|
||||
role="tab"
|
||||
:class="{ 'active': index === $store.whatsNew.currentIndex }"
|
||||
:aria-label="$store.whatsNew.dotLabel(index)"
|
||||
:aria-selected="index === $store.whatsNew.currentIndex ? 'true' : 'false'"
|
||||
@click="$store.whatsNew.goTo(index)"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="whats-new-footer-actions">
|
||||
<button type="button" class="btn btn-cancel" @click="$store.whatsNew.skip()">Skip</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-field"
|
||||
x-show="!$store.whatsNew.isFirst()"
|
||||
@click="$store.whatsNew.previous()"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button type="button" class="btn btn-ok" @click="$store.whatsNew.next()">
|
||||
<span x-text="$store.whatsNew.isLast() ? 'Done' : 'Next'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-inner.whats-new-modal {
|
||||
width: min(92vw, 760px);
|
||||
max-height: min(88vh, 820px);
|
||||
}
|
||||
|
||||
.whats-new-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.whats-new-media-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1476 / 842;
|
||||
min-height: 180px;
|
||||
max-height: min(44vh, 360px);
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
|
||||
border-radius: 7px;
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, color-mix(in srgb, #4248f1 18%, transparent), transparent 34%),
|
||||
color-mix(in srgb, var(--color-panel) 78%, #000 22%);
|
||||
}
|
||||
|
||||
.whats-new-media {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: color-mix(in srgb, var(--color-panel) 82%, #000 18%);
|
||||
}
|
||||
|
||||
.whats-new-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.whats-new-eyebrow {
|
||||
color: color-mix(in srgb, #2196f3 86%, var(--color-text));
|
||||
font-size: 0.74rem;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.whats-new-copy h3 {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: clamp(1.35rem, 2.5vw, 1.8rem);
|
||||
font-weight: 760;
|
||||
line-height: 1.12;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.whats-new-summary {
|
||||
max-width: 62ch;
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.whats-new-bullets {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin: 0.25rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.whats-new-bullets li {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.whats-new-bullets li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.58em;
|
||||
width: 0.42rem;
|
||||
height: 0.42rem;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, #4248f1 72%, #2196f3 28%);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, #4248f1 18%, transparent);
|
||||
}
|
||||
|
||||
.whats-new-footer {
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.whats-new-footer-left,
|
||||
.whats-new-footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.whats-new-progress-label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.whats-new-progress-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.whats-new-dot {
|
||||
width: 0.48rem;
|
||||
height: 0.48rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-text-muted) 45%, transparent);
|
||||
cursor: pointer;
|
||||
transition: transform 0.16s ease, background-color 0.16s ease, width 0.16s ease;
|
||||
}
|
||||
|
||||
.whats-new-dot.active {
|
||||
width: 1.2rem;
|
||||
background: color-mix(in srgb, #4248f1 74%, #2196f3 26%);
|
||||
}
|
||||
|
||||
.whats-new-dot:hover {
|
||||
transform: translateY(-1px);
|
||||
background: color-mix(in srgb, #2196f3 72%, var(--color-text));
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.modal-inner.whats-new-modal {
|
||||
width: min(94vw, 760px);
|
||||
}
|
||||
|
||||
.whats-new-media-panel {
|
||||
min-height: 150px;
|
||||
max-height: 32vh;
|
||||
}
|
||||
|
||||
.whats-new-footer,
|
||||
.whats-new-footer-left,
|
||||
.whats-new-footer-actions {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.whats-new-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.whats-new-footer-left {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.whats-new-footer-actions {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="loading">Opening What's New...</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue