From 7b044d22efaf82e8a09828d096353a37acc4ffa1 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 07:09:31 +0000 Subject: [PATCH] feat: implement three-mode UI system (Easy / Pro / Chat) Add switchable UI modes with conditional rendering: - Easy mode: goal-based interface with 7 guided workflows - Pro mode: current technical interface preserved with Quick Start Goals - Chat mode: AIUI placeholder for future integration New components: ModeSwitcher, EasyHome, GoalDetail wizard, Chat placeholder New stores: uiMode (mode persistence), goals (progress tracking) New data: goal definitions catalog, helpTree goals section Modified: Dashboard (reactive nav), Home (mode dispatcher), Settings (mode picker), Router (goal/chat routes), Spotlight (goal search integration) Co-Authored-By: Claude Opus 4.6 --- neode-ui/src/components/EasyHome.vue | 78 ++++++ neode-ui/src/components/ModeSwitcher.vue | 26 ++ neode-ui/src/components/SpotlightSearch.vue | 2 +- neode-ui/src/data/goals.ts | 262 +++++++++++++++++++ neode-ui/src/data/helpTree.ts | 19 +- neode-ui/src/router/index.ts | 10 + neode-ui/src/stores/goals.ts | 105 ++++++++ neode-ui/src/stores/spotlight.ts | 2 +- neode-ui/src/stores/uiMode.ts | 33 +++ neode-ui/src/style.css | 89 +++++++ neode-ui/src/types/api.ts | 3 + neode-ui/src/types/goals.ts | 32 +++ neode-ui/src/views/Chat.vue | 29 +++ neode-ui/src/views/Dashboard.vue | 137 +++++----- neode-ui/src/views/GoalDetail.vue | 266 ++++++++++++++++++++ neode-ui/src/views/Home.vue | 63 +++-- neode-ui/src/views/Settings.vue | 55 ++++ 17 files changed, 1108 insertions(+), 103 deletions(-) create mode 100644 neode-ui/src/components/EasyHome.vue create mode 100644 neode-ui/src/components/ModeSwitcher.vue create mode 100644 neode-ui/src/data/goals.ts create mode 100644 neode-ui/src/stores/goals.ts create mode 100644 neode-ui/src/stores/uiMode.ts create mode 100644 neode-ui/src/types/goals.ts create mode 100644 neode-ui/src/views/Chat.vue create mode 100644 neode-ui/src/views/GoalDetail.vue diff --git a/neode-ui/src/components/EasyHome.vue b/neode-ui/src/components/EasyHome.vue new file mode 100644 index 00000000..a7e924ef --- /dev/null +++ b/neode-ui/src/components/EasyHome.vue @@ -0,0 +1,78 @@ + + + diff --git a/neode-ui/src/components/ModeSwitcher.vue b/neode-ui/src/components/ModeSwitcher.vue new file mode 100644 index 00000000..7929b5e9 --- /dev/null +++ b/neode-ui/src/components/ModeSwitcher.vue @@ -0,0 +1,26 @@ + + + diff --git a/neode-ui/src/components/SpotlightSearch.vue b/neode-ui/src/components/SpotlightSearch.vue index d3e04eba..8d7e7e46 100644 --- a/neode-ui/src/components/SpotlightSearch.vue +++ b/neode-ui/src/components/SpotlightSearch.vue @@ -237,7 +237,7 @@ function selectHelpItem(section: { id: string }, item: { id: string; label: stri } } -function selectRecent(item: { id: string; label: string; path?: string; type: 'navigate' | 'learn' | 'action' }) { +function selectRecent(item: { id: string; label: string; path?: string; type: 'navigate' | 'learn' | 'action' | 'goal' }) { spotlightStore.close() if (item.path === '__cli__') { cliStore.open() diff --git a/neode-ui/src/data/goals.ts b/neode-ui/src/data/goals.ts new file mode 100644 index 00000000..3ff03e37 --- /dev/null +++ b/neode-ui/src/data/goals.ts @@ -0,0 +1,262 @@ +import type { GoalDefinition } from '@/types/goals' + +export const GOALS: GoalDefinition[] = [ + { + id: 'open-a-shop', + title: 'Open a Shop', + subtitle: 'Accept Bitcoin payments with your own online store', + icon: 'shop', + category: 'commerce', + requiredApps: ['bitcoin-knots', 'lnd', 'btcpay-server'], + steps: [ + { + id: 'install-bitcoin', + title: 'Install Bitcoin Node', + description: 'Bitcoin Knots validates transactions and maintains the blockchain on your hardware. This is the foundation of your sovereign payment stack.', + appId: 'bitcoin-knots', + action: 'install', + isAutomatic: true, + }, + { + id: 'install-lnd', + title: 'Install Lightning Network', + description: 'LND enables instant, low-fee Bitcoin payments through payment channels. Your customers can pay in seconds.', + appId: 'lnd', + action: 'install', + isAutomatic: true, + }, + { + id: 'install-btcpay', + title: 'Install BTCPay Server', + description: 'BTCPay Server is your self-hosted payment processor. Create invoices, manage your store, and accept payments — all without middlemen.', + appId: 'btcpay-server', + action: 'install', + isAutomatic: true, + }, + { + id: 'configure-store', + title: 'Set Up Your Store', + description: 'Create your store, set your currency, and customize your payment page. BTCPay will open so you can configure everything.', + action: 'configure', + isAutomatic: false, + }, + ], + estimatedTime: '~45 min + sync time', + difficulty: 'beginner', + }, + { + id: 'accept-payments', + title: 'Accept Payments', + subtitle: 'Receive Bitcoin and Lightning payments directly', + icon: 'payments', + category: 'payments', + requiredApps: ['bitcoin-knots', 'lnd'], + steps: [ + { + id: 'install-bitcoin', + title: 'Install Bitcoin Node', + description: 'Your own Bitcoin node verifies every transaction independently. No trust required.', + appId: 'bitcoin-knots', + action: 'install', + isAutomatic: true, + }, + { + id: 'install-lnd', + title: 'Install Lightning Network', + description: 'Lightning enables instant payments with tiny fees. Perfect for everyday transactions.', + appId: 'lnd', + action: 'install', + isAutomatic: true, + }, + { + id: 'open-channel', + title: 'Open a Lightning Channel', + description: 'Open your first payment channel to start sending and receiving Lightning payments. LND will guide you through it.', + action: 'configure', + isAutomatic: false, + }, + ], + estimatedTime: '~30 min + sync time', + difficulty: 'beginner', + }, + { + id: 'store-photos', + title: 'Store My Photos', + subtitle: 'Private photo backup and gallery on your own hardware', + icon: 'photos', + category: 'storage', + requiredApps: ['immich'], + steps: [ + { + id: 'install-immich', + title: 'Install Immich', + description: 'Immich is a self-hosted photo and video management solution. It looks and feels like Google Photos, but your data stays on your server.', + appId: 'immich', + action: 'install', + isAutomatic: true, + }, + { + id: 'configure-immich', + title: 'Create Your Account', + description: 'Set up your Immich account and configure your photo library. Quick and simple.', + action: 'configure', + isAutomatic: false, + }, + { + id: 'mobile-sync', + title: 'Connect Your Phone', + description: 'Download the Immich app on your phone and scan the QR code to start automatic photo backup.', + action: 'info', + isAutomatic: false, + }, + ], + estimatedTime: '~15 min', + difficulty: 'beginner', + }, + { + id: 'store-files', + title: 'Store My Files', + subtitle: 'Personal cloud storage and file sync', + icon: 'files', + category: 'storage', + requiredApps: ['nextcloud'], + steps: [ + { + id: 'install-nextcloud', + title: 'Install Cloud Storage', + description: 'Nextcloud gives you a full cloud storage platform — files, calendars, contacts, and more. Like Dropbox, but sovereign.', + appId: 'nextcloud', + action: 'install', + isAutomatic: true, + }, + { + id: 'configure-nextcloud', + title: 'Set Up Your Cloud', + description: 'Create your admin account and configure storage. Nextcloud will open for you to complete setup.', + action: 'configure', + isAutomatic: false, + }, + { + id: 'sync-setup', + title: 'Sync Your Devices', + description: 'Install the Nextcloud app on your phone and computer to keep your files in sync across all devices.', + action: 'info', + isAutomatic: false, + }, + ], + estimatedTime: '~20 min', + difficulty: 'beginner', + }, + { + id: 'run-lightning-node', + title: 'Run a Lightning Node', + subtitle: 'Route payments and earn sats on the Lightning Network', + icon: 'lightning', + category: 'network', + requiredApps: ['bitcoin-knots', 'lnd'], + steps: [ + { + id: 'install-bitcoin', + title: 'Install Bitcoin Node', + description: 'The Bitcoin blockchain is the settlement layer. Your node needs to sync the full chain before Lightning can start.', + appId: 'bitcoin-knots', + action: 'install', + isAutomatic: true, + }, + { + id: 'install-lnd', + title: 'Install LND', + description: 'LND is a full Lightning Network node. You can route payments for others and earn routing fees.', + appId: 'lnd', + action: 'install', + isAutomatic: true, + }, + { + id: 'open-channels', + title: 'Open Payment Channels', + description: 'Open channels with well-connected nodes to start routing payments. More channels means more routing opportunities.', + action: 'configure', + isAutomatic: false, + }, + { + id: 'verify-routing', + title: 'Verify Node is Routing', + description: 'Check that your node is visible on the network and ready to route payments.', + action: 'verify', + isAutomatic: true, + }, + ], + estimatedTime: '~40 min + sync time', + difficulty: 'intermediate', + }, + { + id: 'create-identity', + title: 'Create My Identity', + subtitle: 'Sovereign digital identity with DID and Nostr', + icon: 'identity', + category: 'identity', + requiredApps: [], + steps: [ + { + id: 'generate-did', + title: 'Generate Your Identity', + description: 'Your server creates a cryptographic identity (DID) that you own and control. No company can revoke it.', + action: 'verify', + isAutomatic: true, + }, + { + id: 'setup-nostr', + title: 'Set Up Nostr Profile', + description: 'Publish your identity to the Nostr network. This lets you sign into Nostr-compatible apps directly from your server.', + action: 'configure', + isAutomatic: false, + }, + { + id: 'export-identity', + title: 'Export Your Identity', + description: 'Save your identity credentials for backup. This is your portable sovereign identity — take it anywhere.', + action: 'info', + isAutomatic: false, + }, + ], + estimatedTime: '~5 min', + difficulty: 'beginner', + }, + { + id: 'back-up-everything', + title: 'Back Up Everything', + subtitle: 'Encrypted backup of your entire node', + icon: 'backup', + category: 'backup', + requiredApps: [], + steps: [ + { + id: 'create-passphrase', + title: 'Create a Passphrase', + description: 'Choose a strong passphrase to encrypt your backup. Without this passphrase, nobody can access your data — not even us.', + action: 'configure', + isAutomatic: false, + }, + { + id: 'create-backup', + title: 'Create Encrypted Backup', + description: 'Your server will create a complete encrypted backup of all your data, keys, and configuration.', + action: 'verify', + isAutomatic: true, + }, + { + id: 'save-backup', + title: 'Save Your Backup', + description: 'Download your encrypted backup file and store it somewhere safe. Consider keeping a copy on a USB drive and in the cloud.', + action: 'info', + isAutomatic: false, + }, + ], + estimatedTime: '~10 min', + difficulty: 'beginner', + }, +] + +export function getGoalById(id: string): GoalDefinition | undefined { + return GOALS.find((g) => g.id === id) +} diff --git a/neode-ui/src/data/helpTree.ts b/neode-ui/src/data/helpTree.ts index c5ec0907..aa7de216 100644 --- a/neode-ui/src/data/helpTree.ts +++ b/neode-ui/src/data/helpTree.ts @@ -16,7 +16,7 @@ export interface SearchableItem { id: string label: string path?: string - type: 'navigate' | 'learn' | 'action' + type: 'navigate' | 'learn' | 'action' | 'goal' section: string content?: string relatedPath?: string @@ -71,6 +71,19 @@ export const helpTree: HelpSection[] = [ { id: 'backup', label: 'Backup & Recovery', path: '/dashboard/settings' }, ], }, + { + id: 'goals', + label: 'Quick Start Goals', + items: [ + { id: 'goal-shop', label: 'Open a Shop', path: '/dashboard/goals/open-a-shop' }, + { id: 'goal-payments', label: 'Accept Payments', path: '/dashboard/goals/accept-payments' }, + { id: 'goal-photos', label: 'Store My Photos', path: '/dashboard/goals/store-photos' }, + { id: 'goal-files', label: 'Store My Files', path: '/dashboard/goals/store-files' }, + { id: 'goal-lightning', label: 'Run a Lightning Node', path: '/dashboard/goals/run-lightning-node' }, + { id: 'goal-identity', label: 'Create My Identity', path: '/dashboard/goals/create-identity' }, + { id: 'goal-backup', label: 'Back Up Everything', path: '/dashboard/goals/back-up-everything' }, + ], + }, ] export function flattenForSearch(): SearchableItem[] { @@ -81,7 +94,9 @@ export function flattenForSearch(): SearchableItem[] { ? 'navigate' : section.id === 'learn' ? 'learn' - : 'action' + : section.id === 'goals' + ? 'goal' + : 'action' for (const item of section.items) { result.push({ id: item.id, diff --git a/neode-ui/src/router/index.ts b/neode-ui/src/router/index.ts index dcb2b2c0..153917a9 100644 --- a/neode-ui/src/router/index.ts +++ b/neode-ui/src/router/index.ts @@ -110,6 +110,16 @@ const router = createRouter({ name: 'settings', component: () => import('../views/Settings.vue'), }, + { + path: 'goals/:goalId', + name: 'goal-detail', + component: () => import('../views/GoalDetail.vue'), + }, + { + path: 'chat', + name: 'chat', + component: () => import('../views/Chat.vue'), + }, // Containers removed: My Apps serves the same purpose. Redirect old links. { path: 'containers', diff --git a/neode-ui/src/stores/goals.ts b/neode-ui/src/stores/goals.ts new file mode 100644 index 00000000..d7aec789 --- /dev/null +++ b/neode-ui/src/stores/goals.ts @@ -0,0 +1,105 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { GoalProgress, GoalStatus } from '@/types/goals' +import { GOALS } from '@/data/goals' +import { useAppStore } from './app' + +const STORAGE_KEY = 'archipelago-goal-progress' + +export const useGoalStore = defineStore('goals', () => { + const progress = ref>({}) + + function load() { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) progress.value = JSON.parse(raw) + } catch { + /* ignore corrupt data */ + } + } + + function save() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(progress.value)) + } + + function getGoalStatus(goalId: string): GoalStatus { + const goal = GOALS.find((g) => g.id === goalId) + if (!goal) return 'not-started' + + // Goals with no required apps use manual progress tracking + if (goal.requiredApps.length === 0) { + return progress.value[goalId]?.status || 'not-started' + } + + const appStore = useAppStore() + const packages = appStore.packages + + const allRunning = goal.requiredApps.every((appId) => + Object.entries(packages).some( + ([pkgId, pkg]) => pkgId === appId && pkg.state === 'running', + ), + ) + if (allRunning) return 'completed' + + const anyInstalled = goal.requiredApps.some((appId) => + Object.keys(packages).some((pkgId) => pkgId === appId), + ) + if (anyInstalled || progress.value[goalId]) return 'in-progress' + + return 'not-started' + } + + const goalStatuses = computed(() => { + const statuses: Record = {} + for (const goal of GOALS) { + statuses[goal.id] = getGoalStatus(goal.id) + } + return statuses + }) + + function startGoal(goalId: string) { + progress.value[goalId] = { + goalId, + status: 'in-progress', + currentStepIndex: 0, + completedSteps: [], + startedAt: Date.now(), + } + save() + } + + function completeStep(goalId: string, stepId: string) { + const p = progress.value[goalId] + if (!p) return + + if (!p.completedSteps.includes(stepId)) { + p.completedSteps.push(stepId) + } + + const goal = GOALS.find((g) => g.id === goalId) + if (goal && p.completedSteps.length >= goal.steps.length) { + p.status = 'completed' + } else { + p.currentStepIndex = Math.min(p.currentStepIndex + 1, (goal?.steps.length ?? 1) - 1) + } + + save() + } + + function resetGoal(goalId: string) { + delete progress.value[goalId] + save() + } + + // Load on store creation + load() + + return { + progress, + goalStatuses, + getGoalStatus, + startGoal, + completeStep, + resetGoal, + } +}) diff --git a/neode-ui/src/stores/spotlight.ts b/neode-ui/src/stores/spotlight.ts index f6714f57..98686577 100644 --- a/neode-ui/src/stores/spotlight.ts +++ b/neode-ui/src/stores/spotlight.ts @@ -9,7 +9,7 @@ export interface RecentItem { id: string label: string path?: string - type: 'navigate' | 'learn' | 'action' + type: 'navigate' | 'learn' | 'action' | 'goal' timestamp: number } diff --git a/neode-ui/src/stores/uiMode.ts b/neode-ui/src/stores/uiMode.ts new file mode 100644 index 00000000..a90ee1b7 --- /dev/null +++ b/neode-ui/src/stores/uiMode.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { UIMode } from '@/types/api' + +const STORAGE_KEY = 'archipelago-ui-mode' + +export const useUIModeStore = defineStore('uiMode', () => { + const mode = ref(loadFromStorage()) + + function loadFromStorage(): UIMode { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'gamer' || stored === 'easy' || stored === 'chat') return stored + return 'gamer' + } + + function syncFromBackend(backendMode: UIMode | undefined) { + if (backendMode && ['gamer', 'easy', 'chat'].includes(backendMode)) { + mode.value = backendMode + localStorage.setItem(STORAGE_KEY, backendMode) + } + } + + function setMode(newMode: UIMode) { + mode.value = newMode + localStorage.setItem(STORAGE_KEY, newMode) + } + + const isGamer = computed(() => mode.value === 'gamer') + const isEasy = computed(() => mode.value === 'easy') + const isChat = computed(() => mode.value === 'chat') + + return { mode, setMode, syncFromBackend, isGamer, isEasy, isChat } +}) diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index 218d58d1..4654bfb7 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -67,6 +67,95 @@ overflow-y: visible; } + /* Mode switcher - sidebar toggle */ + .mode-switcher { + display: flex; + gap: 2px; + padding: 3px; + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + } + + .mode-switcher-btn { + flex: 1; + padding: 0.375rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.45); + transition: all 0.25s ease; + cursor: pointer; + text-align: center; + border: none; + background: transparent; + } + + .mode-switcher-btn:hover { + color: rgba(255, 255, 255, 0.75); + } + + .mode-switcher-btn-active { + background: rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.95); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); + } + + /* Goal cards */ + .goal-card { + cursor: pointer; + transition: all 0.3s ease; + } + + .goal-card:hover { + transform: translateY(-2px); + } + + .goal-status-badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.625rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + } + + .goal-status-badge-not-started { + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.5); + } + + .goal-status-badge-in-progress { + background: rgba(251, 146, 60, 0.15); + color: #fb923c; + } + + .goal-status-badge-completed { + background: rgba(74, 222, 128, 0.15); + color: #4ade80; + } + + /* Goal wizard steps */ + .goal-step { + padding: 1rem 1.25rem; + border-left: 3px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + } + + .goal-step-active { + border-left-color: #fb923c; + background: rgba(251, 146, 60, 0.05); + } + + .goal-step-completed { + border-left-color: #4ade80; + } + + .goal-step-pending { + opacity: 0.5; + } + .glass-button { position: relative; display: inline-flex; diff --git a/neode-ui/src/types/api.ts b/neode-ui/src/types/api.ts index ec58d092..6838a155 100644 --- a/neode-ui/src/types/api.ts +++ b/neode-ui/src/types/api.ts @@ -28,11 +28,14 @@ export interface StatusInfo { 'update-progress': number | null } +export type UIMode = 'gamer' | 'easy' | 'chat' + export interface UIData { name: string | null 'ack-welcome': string marketplace: UIMarketplaceData theme: string + mode?: UIMode } export interface UIMarketplaceData { diff --git a/neode-ui/src/types/goals.ts b/neode-ui/src/types/goals.ts new file mode 100644 index 00000000..8fd88338 --- /dev/null +++ b/neode-ui/src/types/goals.ts @@ -0,0 +1,32 @@ +// Goal-based workflow types for Easy mode + +export interface GoalDefinition { + id: string + title: string + subtitle: string + icon: string + category: 'commerce' | 'payments' | 'storage' | 'identity' | 'network' | 'backup' + requiredApps: string[] + steps: GoalStep[] + estimatedTime: string + difficulty: 'beginner' | 'intermediate' +} + +export interface GoalStep { + id: string + title: string + description: string + appId?: string + action: 'install' | 'configure' | 'verify' | 'info' + isAutomatic: boolean +} + +export type GoalStatus = 'not-started' | 'in-progress' | 'completed' | 'error' + +export interface GoalProgress { + goalId: string + status: GoalStatus + currentStepIndex: number + completedSteps: string[] + startedAt?: number +} diff --git a/neode-ui/src/views/Chat.vue b/neode-ui/src/views/Chat.vue new file mode 100644 index 00000000..136558b5 --- /dev/null +++ b/neode-ui/src/views/Chat.vue @@ -0,0 +1,29 @@ + + + diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue index 0fca3090..d3e55690 100644 --- a/neode-ui/src/views/Dashboard.vue +++ b/neode-ui/src/views/Dashboard.vue @@ -82,7 +82,11 @@ -