From 6115a685503bc7fd94b23a5a360d96373996717a Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 6 Apr 2026 00:07:26 -0700 Subject: [PATCH] feat: Vue SPA demo mode support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useToast.ts: global reactive toast singleton for cross-component toasts - App.vue: sticky demo mode banner + global toast slot - router: bypass wizard gate entirely in demo mode (pre-seeded data) - ApplyWorkspace, CompanyResearchModal: guard generate() in demo mode - fineTune store: guard submitJob() in demo mode - ui_switcher.py: remove Vue→Streamlit fallback in demo mode (now handled natively) All LLM-triggering actions show a toast and no-op in demo mode. Backend already blocks inference via DEMO_MODE env; Vue layer adds UX signal. Closes #46 --- app/components/ui_switcher.py | 6 -- web/src/App.vue | 64 +++++++++++++++++++++ web/src/components/ApplyWorkspace.vue | 4 ++ web/src/components/CompanyResearchModal.vue | 5 ++ web/src/composables/useToast.ts | 20 +++++++ web/src/router/index.ts | 3 + web/src/stores/settings/fineTune.ts | 3 + 7 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 web/src/composables/useToast.ts diff --git a/app/components/ui_switcher.py b/app/components/ui_switcher.py index 33ed955..54d0788 100644 --- a/app/components/ui_switcher.py +++ b/app/components/ui_switcher.py @@ -124,12 +124,6 @@ def sync_ui_cookie(yaml_path: Path, tier: str) -> None: # UI components must not crash the app — silent fallback to default pref = "streamlit" - # Demo mode: Vue SPA has no demo data wiring — always serve Streamlit. - # (The tier downgrade check below is skipped in demo mode, but we must - # also block the Vue navigation itself so Caddy doesn't route to a blank SPA.) - if pref == "vue" and _DEMO_MODE: - pref = "streamlit" - # Tier downgrade protection (skip in demo — demo bypasses tier gate) if pref == "vue" and not _DEMO_MODE and not can_use(tier, "vue_ui_beta"): if profile is not None: diff --git a/web/src/App.vue b/web/src/App.vue index 28efa08..fb16f04 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -6,7 +6,20 @@
+ + +
+ 👁 Demo mode — changes are not saved and AI features are disabled. +
+ + + + +
+ {{ toast.message.value }} +
+
@@ -17,13 +30,17 @@ import { RouterView, useRoute } from 'vue-router' import { useMotion } from './composables/useMotion' import { useHackerMode, useKonamiCode } from './composables/useEasterEgg' import { useTheme } from './composables/useTheme' +import { useToast } from './composables/useToast' import AppNav from './components/AppNav.vue' +import { useAppConfigStore } from './stores/appConfig' import { useDigestStore } from './stores/digest' const motion = useMotion() const route = useRoute() const { toggle, restore } = useHackerMode() const { initTheme } = useTheme() +const toast = useToast() +const config = useAppConfigStore() const digestStore = useDigestStore() const isWizard = computed(() => route.path.startsWith('/setup')) @@ -110,4 +127,51 @@ body { margin-left: 0; padding-bottom: 0; } + +/* Demo mode banner — sticky top bar */ +.demo-banner { + position: sticky; + top: 0; + z-index: 200; + background: var(--color-warning); + color: #1a1a1a; /* forced dark — warning bg is always light enough */ + text-align: center; + font-size: 0.85rem; + font-weight: 600; + padding: 6px var(--space-4, 16px); + letter-spacing: 0.01em; +} + +/* Global toast — bottom-center, above tab bar */ +.global-toast { + position: fixed; + bottom: calc(72px + env(safe-area-inset-bottom)); + left: 50%; + transform: translateX(-50%); + background: var(--color-surface-raised, #2a3650); + color: var(--color-text, #eaeff8); + padding: 10px 20px; + border-radius: var(--radius-md, 8px); + font-size: 0.9rem; + font-weight: 500; + box-shadow: 0 4px 16px rgba(0,0,0,0.25); + white-space: nowrap; + z-index: 9000; + pointer-events: none; +} + +.global-toast-enter-active, .global-toast-leave-active { + transition: opacity 220ms ease, transform 220ms ease; +} +.global-toast-enter-from, .global-toast-leave-to { + opacity: 0; + transform: translateX(-50%) translateY(8px); +} + +@media (min-width: 1024px) { + .global-toast { + bottom: calc(24px + env(safe-area-inset-bottom)); + left: calc(50% + var(--sidebar-width, 220px) / 2); + } +} diff --git a/web/src/components/ApplyWorkspace.vue b/web/src/components/ApplyWorkspace.vue index a8e3a8e..6560b6a 100644 --- a/web/src/components/ApplyWorkspace.vue +++ b/web/src/components/ApplyWorkspace.vue @@ -283,9 +283,12 @@