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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 07:09:31 +00:00
co-authored by Claude Opus 4.6
parent 486fc39249
commit 7b044d22ef
17 changed files with 1108 additions and 103 deletions
+29
View File
@@ -0,0 +1,29 @@
<template>
<div class="flex flex-col items-center justify-center min-h-[60vh]">
<div class="glass-card p-12 max-w-lg w-full text-center">
<div class="w-16 h-16 mx-auto mb-6 rounded-full bg-white/10 flex items-center justify-center">
<svg class="w-8 h-8 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</svg>
</div>
<h2 class="text-2xl font-semibold text-white mb-2">AI Assistant</h2>
<p class="text-white/60 mb-8 leading-relaxed">
Conversational interface coming soon. Talk to your node, ask questions,
and manage everything through natural language.
</p>
<div class="bg-white/5 border border-white/10 rounded-xl p-4">
<input
type="text"
disabled
placeholder="What would you like to do?"
class="w-full bg-transparent text-white/30 outline-none placeholder-white/30 cursor-not-allowed"
/>
</div>
<p class="text-xs text-white/30 mt-4">AIUI integration in development</p>
</div>
</div>
</template>
<script setup lang="ts">
// Chat mode placeholder — will integrate AIUI here
</script>
+70 -67
View File
@@ -82,7 +82,11 @@
</div>
</div>
<nav class="sidebar-nav flex-1 min-h-0 space-y-2 p-6 pt-4">
<div class="px-6 pt-4 pb-2 shrink-0">
<ModeSwitcher />
</div>
<nav class="sidebar-nav flex-1 min-h-0 space-y-2 p-6 pt-2">
<RouterLink
v-for="(item, idx) in desktopNavItems"
:key="item.path"
@@ -315,8 +319,12 @@ import { useLoginTransitionStore } from '../stores/loginTransition'
import AnimatedLogo from '@/components/AnimatedLogo.vue'
import OnlineStatusPill from '@/components/OnlineStatusPill.vue'
import ControllerIndicator from '@/components/ControllerIndicator.vue'
import ModeSwitcher from '@/components/ModeSwitcher.vue'
import { useUIModeStore } from '@/stores/uiMode'
import { playDashboardLoadOomph } from '@/composables/useLoginSounds'
const uiMode = useUIModeStore()
const router = useRouter()
const route = useRoute()
const store = useAppStore()
@@ -517,76 +525,70 @@ const isOffline = computed(() => 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<string, string[]> = {
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',
+266
View File
@@ -0,0 +1,266 @@
<template>
<div class="pb-6">
<!-- Back button -->
<button @click="goBack" class="flex items-center gap-2 text-white/60 hover:text-white mb-6 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<span>Back to Goals</span>
</button>
<!-- Goal not found -->
<div v-if="!goal" class="glass-card p-12 text-center">
<p class="text-white/70">Goal not found.</p>
</div>
<!-- Goal wizard -->
<template v-else>
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">{{ goal.title }}</h1>
<p class="text-white/70">{{ goal.subtitle }}</p>
</div>
<!-- Progress bar -->
<div class="mb-8">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-white/60">Step {{ currentStepDisplay }} of {{ goal.steps.length }}</span>
<span class="goal-status-badge" :class="statusBadgeClass">{{ statusLabel }}</span>
</div>
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all duration-500 ease-out"
:class="overallStatus === 'completed' ? 'bg-green-400' : 'bg-orange-400'"
:style="{ width: `${progressPercent}%` }"
/>
</div>
</div>
<!-- Sovereignty patience message for bitcoin-dependent goals -->
<div
v-if="showSyncMessage"
class="glass-card p-6 mb-6 border-l-4 border-orange-400"
>
<h3 class="text-lg font-semibold text-white mb-1">Sovereignty takes a little patience</h3>
<p class="text-white/60 text-sm leading-relaxed">
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.
</p>
</div>
<!-- Steps -->
<div class="space-y-3">
<div
v-for="(step, idx) in goal.steps"
:key="step.id"
class="glass-card p-0 overflow-hidden"
>
<div
class="goal-step"
:class="{
'goal-step-completed': isStepCompleted(step),
'goal-step-active': idx === activeStepIndex && overallStatus !== 'completed',
'goal-step-pending': idx > activeStepIndex && !isStepCompleted(step),
}"
>
<div class="flex items-start gap-4">
<!-- Step indicator -->
<div class="mt-0.5 shrink-0">
<div v-if="isStepCompleted(step)" class="w-6 h-6 rounded-full bg-green-500/20 flex items-center justify-center">
<svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<div v-else-if="idx === activeStepIndex && isInstalling" class="w-6 h-6 rounded-full bg-orange-500/20 flex items-center justify-center">
<svg class="w-4 h-4 text-orange-400 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
</div>
<div v-else class="w-6 h-6 rounded-full bg-white/10 flex items-center justify-center">
<span class="text-xs text-white/40 font-medium">{{ idx + 1 }}</span>
</div>
</div>
<!-- Step content -->
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-white/90 mb-1">{{ step.title }}</h3>
<p class="text-sm text-white/55 leading-relaxed">{{ step.description }}</p>
<!-- Action button for active step -->
<div v-if="idx === activeStepIndex && overallStatus !== 'completed'" class="mt-4">
<button
v-if="step.action === 'install' && step.appId && !isAppInstalled(step.appId)"
@click="installApp(step)"
:disabled="isInstalling"
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium"
>
{{ isInstalling ? 'Installing...' : `Install ${step.title.replace('Install ', '')}` }}
</button>
<button
v-else-if="step.action === 'configure'"
@click="openConfigureStep(step)"
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium"
>
Open &amp; Configure
</button>
<button
v-else-if="step.action === 'info'"
@click="completeInfoStep(step)"
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium"
>
I've Done This
</button>
<button
v-else-if="isStepCompleted(step) || isAppInstalled(step.appId || '')"
disabled
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium opacity-50"
>
Complete
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Completion -->
<div v-if="overallStatus === 'completed'" class="glass-card p-8 mt-6 text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-green-500/20 flex items-center justify-center">
<svg class="w-8 h-8 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-xl font-semibold text-white mb-2">All Set!</h2>
<p class="text-white/60 mb-6">{{ goal.title }} is ready to go.</p>
<RouterLink to="/dashboard/apps" class="glass-button rounded-lg px-6 py-3 font-medium">
View My Services
</RouterLink>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRoute, useRouter, RouterLink } from 'vue-router'
import { useAppStore } from '@/stores/app'
import { useGoalStore } from '@/stores/goals'
import { getGoalById } from '@/data/goals'
import type { GoalStep } from '@/types/goals'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const goalStore = useGoalStore()
const goalId = computed(() => route.params.goalId as string)
const goal = computed(() => getGoalById(goalId.value))
const isInstalling = ref(false)
const overallStatus = computed(() => goalStore.getGoalStatus(goalId.value))
const completedSteps = computed(() => {
if (!goal.value) return new Set<string>()
const completed = new Set<string>()
for (const step of goal.value.steps) {
if (step.appId && isAppInstalled(step.appId)) {
completed.add(step.id)
}
if (goalStore.progress[goalId.value]?.completedSteps.includes(step.id)) {
completed.add(step.id)
}
}
return completed
})
const activeStepIndex = computed(() => {
if (!goal.value) return 0
const steps = goal.value.steps
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
if (step && !completedSteps.value.has(step.id)) return i
}
return steps.length - 1
})
const currentStepDisplay = computed(() => Math.min(activeStepIndex.value + 1, goal.value?.steps.length || 1))
const progressPercent = computed(() => {
if (!goal.value) return 0
return Math.round((completedSteps.value.size / goal.value.steps.length) * 100)
})
const statusLabel = computed(() => {
if (overallStatus.value === 'completed') return 'Completed'
if (overallStatus.value === 'in-progress') return 'In Progress'
return 'Not Started'
})
const statusBadgeClass = computed(() => {
if (overallStatus.value === 'completed') return 'goal-status-badge-completed'
if (overallStatus.value === 'in-progress') return 'goal-status-badge-in-progress'
return 'goal-status-badge-not-started'
})
const showSyncMessage = computed(() => {
if (!goal.value) return false
const hasBitcoin = goal.value.requiredApps.includes('bitcoin-knots')
const bitcoinNotRunning = !isAppRunning('bitcoin-knots')
return hasBitcoin && bitcoinNotRunning && overallStatus.value !== 'completed'
})
function isStepCompleted(step: GoalStep): boolean {
return completedSteps.value.has(step.id)
}
function isAppInstalled(appId: string): boolean {
return Object.keys(appStore.packages).some((pkgId) => pkgId === appId)
}
function isAppRunning(appId: string): boolean {
return Object.entries(appStore.packages).some(
([pkgId, pkg]) => pkgId === appId && pkg.state === 'running',
)
}
async function installApp(step: GoalStep) {
if (!step.appId) return
isInstalling.value = true
// Start goal tracking if not already started
if (!goalStore.progress[goalId.value]) {
goalStore.startGoal(goalId.value)
}
try {
await appStore.installPackage(step.appId, '', 'latest')
goalStore.completeStep(goalId.value, step.id)
} catch (err) {
console.error('[GoalDetail] Install failed:', err)
} finally {
isInstalling.value = false
}
}
function openConfigureStep(step: GoalStep) {
// Mark step as complete when user acknowledges they've configured
goalStore.completeStep(goalId.value, step.id)
// If there's an app to open, navigate to it
if (step.appId) {
router.push(`/dashboard/apps/${step.appId}`)
}
}
function completeInfoStep(step: GoalStep) {
if (!goalStore.progress[goalId.value]) {
goalStore.startGoal(goalId.value)
}
goalStore.completeStep(goalId.value, step.id)
}
function goBack() {
router.push('/dashboard')
}
</script>
+31 -32
View File
@@ -11,8 +11,16 @@
</div>
</div>
<!-- Section Overviews - 2advanced staggered animation (hidden until typing starts, then animate with typing) -->
<!-- Easy Mode: Goal-based cards -->
<EasyHome
v-if="uiMode.isEasy"
:show="!showWelcomeBlock || animateCards"
:animate="animateCards"
/>
<!-- Pro Mode: Section overview cards -->
<div
v-else-if="uiMode.isGamer"
class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8 transition-opacity duration-300"
:class="{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
>
@@ -219,43 +227,28 @@
</div>
</div>
<!-- Quick Actions - Hidden for now -->
<!--
<div class="path-option-card cursor-default px-6 py-6">
<h2 class="text-xl font-semibold text-white/96 mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Quick Start Goals - shown in Pro mode below the overview cards -->
<div v-if="uiMode.isGamer" class="path-option-card cursor-default px-6 py-6">
<h2 class="text-xl font-semibold text-white/96 mb-2">Quick Start Goals</h2>
<p class="text-sm text-white/60 mb-4">Not sure where to start? Try a guided setup.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<RouterLink
to="/dashboard/marketplace"
v-for="goal in topGoals"
:key="goal.id"
:to="`/dashboard/goals/${goal.id}`"
class="path-action-button path-action-button--continue flex items-center justify-center gap-3"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
<span>Browse App Store</span>
</RouterLink>
<RouterLink
to="/dashboard/apps"
class="path-action-button path-action-button--continue flex items-center justify-center gap-3"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
<span>Manage My Apps</span>
</RouterLink>
<RouterLink
to="/dashboard/server"
class="path-action-button path-action-button--continue flex items-center justify-center gap-3"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
</svg>
<span>Server Settings</span>
<span>{{ goal.title }}</span>
</RouterLink>
</div>
</div>
-->
<!-- Chat Mode: redirect to Chat view -->
<div v-if="uiMode.isChat" class="flex flex-col items-center justify-center min-h-[40vh]">
<RouterLink to="/dashboard/chat" class="glass-button rounded-lg px-8 py-4 text-lg font-medium">
Open AI Assistant
</RouterLink>
</div>
</div>
</template>
@@ -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()
+55
View File
@@ -186,6 +186,37 @@
</button>
</div>
<!-- Interface Mode Section -->
<div class="path-option-card cursor-default px-6 py-6 mb-6">
<h2 class="text-xl font-semibold text-white/96 mb-2">Interface Mode</h2>
<p class="text-sm text-white/60 mb-6">Choose how you want to interact with your node.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
v-for="m in interfaceModes"
:key="m.id"
@click="uiMode.setMode(m.id)"
class="path-option-card text-left p-5"
:class="{ 'path-option-card--selected': uiMode.mode === m.id }"
>
<div class="flex items-center gap-3 mb-3">
<svg class="w-6 h-6 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-for="(path, index) in m.iconPaths"
:key="index"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="path"
/>
</svg>
<h3 class="text-lg font-semibold text-white/96">{{ m.label }}</h3>
</div>
<p class="text-sm text-white/60 leading-relaxed">{{ m.description }}</p>
</button>
</div>
</div>
<!-- System Section -->
<div class="path-option-card cursor-default px-6 py-6">
<h2 class="text-xl font-semibold text-white/96 mb-4">System</h2>
@@ -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')