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 @@ + + + + + + {{ goalIcon(goal.icon) }} + + + + + {{ statusLabel(goal.id) }} + + + + {{ goal.title }} + {{ goal.subtitle }} + + + {{ goal.estimatedTime }} + + {{ goal.difficulty === 'beginner' ? 'Beginner' : 'Intermediate' }} + + + + + + + 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 @@ + + + + {{ m.label }} + + + + + 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 @@ + + + + + + + + + AI Assistant + + Conversational interface coming soon. Talk to your node, ask questions, + and manage everything through natural language. + + + + + AIUI integration in development + + + + + 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 @@ - + + + + + store.isOffline) const isRestarting = computed(() => store.isRestarting) const isShuttingDown = computed(() => store.isShuttingDown) -// Desktop navigation items -const desktopNavItems = [ - { - path: '/dashboard', - label: 'Home', - icon: 'home', - }, - { - path: '/dashboard/apps', - label: 'My Apps', - icon: 'apps', - }, - { - path: '/dashboard/marketplace', - label: 'App Store', - icon: 'marketplace', - }, - { - path: '/dashboard/cloud', - label: 'Cloud', - icon: 'cloud', - }, - { - path: '/dashboard/server', - label: 'Network', - icon: 'server', - }, - { - path: '/dashboard/web5', - label: 'Web5', - icon: 'web5', - }, - { - path: '/dashboard/settings', - label: 'Settings', - icon: 'settings', - }, +// Navigation items — reactive based on UI mode +interface NavItem { + path: string + label: string + icon: string + isCombined?: boolean +} + +const gamerDesktopNav: NavItem[] = [ + { path: '/dashboard', label: 'Home', icon: 'home' }, + { path: '/dashboard/apps', label: 'My Apps', icon: 'apps' }, + { path: '/dashboard/marketplace', label: 'App Store', icon: 'marketplace' }, + { path: '/dashboard/cloud', label: 'Cloud', icon: 'cloud' }, + { path: '/dashboard/server', label: 'Network', icon: 'server' }, + { path: '/dashboard/web5', label: 'Web5', icon: 'web5' }, + { path: '/dashboard/settings', label: 'Settings', icon: 'settings' }, ] -// Mobile navigation items (Apps and App Store combined) -const mobileNavItems = [ - { - path: '/dashboard', - label: 'Home', - icon: 'home', - }, - { - path: '/dashboard/apps', - label: 'Apps', - icon: 'apps', - isCombined: true, // This combines apps and marketplace - }, - { - path: '/dashboard/cloud', - label: 'Network', - icon: 'server', - isCombined: true, // This combines server and cloud - }, - { - path: '/dashboard/web5', - label: 'Web5', - icon: 'web5', - }, - { - path: '/dashboard/settings', - label: 'Settings', - icon: 'settings', - }, +const easyDesktopNav: NavItem[] = [ + { path: '/dashboard', label: 'Home', icon: 'home' }, + { path: '/dashboard/apps', label: 'My Services', icon: 'apps' }, + { path: '/dashboard/settings', label: 'Settings', icon: 'settings' }, ] +const chatDesktopNav: NavItem[] = [ + { path: '/dashboard', label: 'Home', icon: 'home' }, + { path: '/dashboard/chat', label: 'Chat', icon: 'chat' }, + { path: '/dashboard/apps', label: 'My Apps', icon: 'apps' }, + { path: '/dashboard/settings', label: 'Settings', icon: 'settings' }, +] + +const desktopNavItems = computed(() => { + if (uiMode.isEasy) return easyDesktopNav + if (uiMode.isChat) return chatDesktopNav + return gamerDesktopNav +}) + +const gamerMobileNav: NavItem[] = [ + { path: '/dashboard', label: 'Home', icon: 'home' }, + { path: '/dashboard/apps', label: 'Apps', icon: 'apps', isCombined: true }, + { path: '/dashboard/cloud', label: 'Network', icon: 'server', isCombined: true }, + { path: '/dashboard/web5', label: 'Web5', icon: 'web5' }, + { path: '/dashboard/settings', label: 'Settings', icon: 'settings' }, +] + +const easyMobileNav: NavItem[] = [ + { path: '/dashboard', label: 'Home', icon: 'home' }, + { path: '/dashboard/apps', label: 'Services', icon: 'apps' }, + { path: '/dashboard/settings', label: 'Settings', icon: 'settings' }, +] + +const chatMobileNav: NavItem[] = [ + { path: '/dashboard', label: 'Home', icon: 'home' }, + { path: '/dashboard/chat', label: 'Chat', icon: 'chat' }, + { path: '/dashboard/apps', label: 'Apps', icon: 'apps' }, + { path: '/dashboard/settings', label: 'Settings', icon: 'settings' }, +] + +const mobileNavItems = computed(() => { + if (uiMode.isEasy) return easyMobileNav + if (uiMode.isChat) return chatMobileNav + return gamerMobileNav +}) + function getIconPath(iconName: string): string[] { const icons: Record = { home: ['M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'], @@ -595,6 +597,7 @@ function getIconPath(iconName: string): string[] { cloud: ['M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z'], server: ['M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01'], web5: ['M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9'], + chat: ['M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z'], settings: [ 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z', 'M15 12a3 3 0 11-6 0 3 3 0 016 0z', diff --git a/neode-ui/src/views/GoalDetail.vue b/neode-ui/src/views/GoalDetail.vue new file mode 100644 index 00000000..6b50328f --- /dev/null +++ b/neode-ui/src/views/GoalDetail.vue @@ -0,0 +1,266 @@ + + + + + + + + Back to Goals + + + + + Goal not found. + + + + + + {{ goal.title }} + {{ goal.subtitle }} + + + + + + Step {{ currentStepDisplay }} of {{ goal.steps.length }} + {{ statusLabel }} + + + + + + + + + Sovereignty takes a little patience + + Your Bitcoin node is syncing the entire blockchain so you don't have to trust anyone else. + This takes 2-3 days on first run. Meanwhile, you can explore your node, set up your identity, or back up your keys. + + + + + + + + + + + + + + + + + + + + + + + {{ idx + 1 }} + + + + + + {{ step.title }} + {{ step.description }} + + + + + {{ isInstalling ? 'Installing...' : `Install ${step.title.replace('Install ', '')}` }} + + + Open & Configure + + + I've Done This + + + Complete + + + + + + + + + + + + + + + + All Set! + {{ goal.title }} is ready to go. + + View My Services + + + + + + + diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index 78014da2..9e808127 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -11,8 +11,16 @@ - + + + + @@ -219,43 +227,28 @@ - - + + Quick Start Goals + Not sure where to start? Try a guided setup. + - - - - Browse App Store - - - - - - - Manage My Apps - - - - - - - Server Settings + {{ goal.title }} - --> + + + + + Open AI Assistant + + @@ -264,8 +257,14 @@ import { computed, ref, watch, onBeforeUnmount } from 'vue' import { RouterLink } from 'vue-router' import { useAppStore } from '../stores/app' import { useLoginTransitionStore } from '../stores/loginTransition' +import { useUIModeStore } from '@/stores/uiMode' import { PackageState } from '../types/api' import { playTypingSound } from '@/composables/useLoginSounds' +import { GOALS } from '@/data/goals' +import EasyHome from '@/components/EasyHome.vue' + +const uiMode = useUIModeStore() +const topGoals = GOALS.slice(0, 3) const store = useAppStore() const loginTransition = useLoginTransitionStore() diff --git a/neode-ui/src/views/Settings.vue b/neode-ui/src/views/Settings.vue index 797430e8..bb593bdb 100644 --- a/neode-ui/src/views/Settings.vue +++ b/neode-ui/src/views/Settings.vue @@ -186,6 +186,37 @@ + + + Interface Mode + Choose how you want to interact with your node. + + + + + + + + {{ m.label }} + + {{ m.description }} + + + + System @@ -204,12 +235,36 @@ import { computed, ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { useAppStore } from '../stores/app' +import { useUIModeStore } from '@/stores/uiMode' import ControllerIndicator from '@/components/ControllerIndicator.vue' import { rpcClient } from '@/api/rpc-client' import { useModalKeyboard } from '@/composables/useModalKeyboard' +import type { UIMode } from '@/types/api' const router = useRouter() const store = useAppStore() +const uiMode = useUIModeStore() + +const interfaceModes: { id: UIMode; label: string; description: string; iconPaths: string[] }[] = [ + { + id: 'easy', + label: 'Easy', + description: 'Goal-based interface. Choose what you want to do, and the system handles the rest.', + iconPaths: ['M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'], + }, + { + id: 'gamer', + label: 'Pro', + description: 'Full control over all services. Configure everything manually with all technical details.', + iconPaths: ['M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z', 'M15 12a3 3 0 11-6 0 3 3 0 016 0z'], + }, + { + id: 'chat', + label: 'Chat', + description: 'Conversational AI interface. Manage your node through natural language. Coming soon.', + iconPaths: ['M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z'], + }, +] const serverName = computed(() => store.serverName) const version = computed(() => store.serverInfo?.version || '0.0.0')
{{ goal.subtitle }}
+ Conversational interface coming soon. Talk to your node, ask questions, + and manage everything through natural language. +
AIUI integration in development
Goal not found.
+ Your Bitcoin node is syncing the entire blockchain so you don't have to trust anyone else. + This takes 2-3 days on first run. Meanwhile, you can explore your node, set up your identity, or back up your keys. +
{{ step.description }}
{{ goal.title }} is ready to go.
Not sure where to start? Try a guided setup.
Choose how you want to interact with your node.
{{ m.description }}