diff --git a/apps/desktop/src/renderer/index.html b/apps/desktop/src/renderer/index.html index c16bdc87..655326da 100644 --- a/apps/desktop/src/renderer/index.html +++ b/apps/desktop/src/renderer/index.html @@ -4,6 +4,14 @@ Multica + + + +
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/connect.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx similarity index 91% rename from apps/desktop/src/renderer/src/pages/onboarding/connect.tsx rename to apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx index 8ca3f7e9..2693b4b6 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/connect.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx @@ -1,13 +1,12 @@ import { useState } from 'react' -import { useNavigate } from 'react-router-dom' import { Button } from '@multica/ui/components/ui/button' import { Input } from '@multica/ui/components/ui/input' import { Badge } from '@multica/ui/components/ui/badge' import { HugeiconsIcon } from '@hugeicons/react' import { ArrowLeft02Icon, Loading03Icon } from '@hugeicons/core-free-icons' -import { useChannels } from '../../hooks/use-channels' -import { TutorialStep } from '../../components/onboarding/tutorial-step' -import { useOnboardingStore } from '../../stores/onboarding' +import { useChannels } from '../../../hooks/use-channels' +import { TutorialStep } from '../../../components/onboarding/tutorial-step' +import { useOnboardingStore } from '../../../stores/onboarding' function statusVariant( status: string @@ -24,8 +23,12 @@ function statusVariant( } } -export default function ConnectStep() { - const navigate = useNavigate() +interface ConnectStepProps { + onNext: () => void + onBack: () => void +} + +export default function ConnectStep({ onNext, onBack }: ConnectStepProps) { const { states, config, saveToken, loading: channelLoading } = useChannels() const { setClientConnected } = useOnboardingStore() @@ -57,16 +60,13 @@ export default function ConnectStep() { setSaving(false) } - const handleContinue = () => navigate('/onboarding/try-it') - const handleBack = () => navigate('/onboarding/setup') - return (
{/* Left column */}
)} -
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/permissions.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/permissions-step.tsx similarity index 85% rename from apps/desktop/src/renderer/src/pages/onboarding/permissions.tsx rename to apps/desktop/src/renderer/src/pages/onboarding/components/permissions-step.tsx index faff86d4..b013886b 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/permissions.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/permissions-step.tsx @@ -1,4 +1,3 @@ -import { useNavigate } from 'react-router-dom' import { Button } from '@multica/ui/components/ui/button' import { FolderOpenIcon, @@ -6,9 +5,9 @@ import { AiBrainIcon, Database01Icon, } from '@hugeicons/core-free-icons' -import { AcknowledgementItem } from '../../components/onboarding/permission-item' -import { PrivacyPanel } from '../../components/onboarding/privacy-panel' -import { useOnboardingStore } from '../../stores/onboarding' +import { AcknowledgementItem } from '../../../components/onboarding/permission-item' +import { PrivacyPanel } from '../../../components/onboarding/privacy-panel' +import { useOnboardingStore } from '../../../stores/onboarding' const acknowledgementItems = [ { @@ -41,15 +40,14 @@ const acknowledgementItems = [ }, ] -export default function PermissionsStep() { - const navigate = useNavigate() +interface PermissionsStepProps { + onNext: () => void +} + +export default function PermissionsStep({ onNext }: PermissionsStepProps) { const { acknowledgements, allAcknowledged, setAcknowledgement } = useOnboardingStore() - const handleContinue = () => { - navigate('/onboarding/setup') - } - return (
{/* Left column — main content, centered both axes */} @@ -83,7 +81,7 @@ export default function PermissionsStep() {
-
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx new file mode 100644 index 00000000..824333c1 --- /dev/null +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx @@ -0,0 +1,65 @@ +import { Button } from '@multica/ui/components/ui/button' +import { MulticaIcon } from '@multica/ui/components/multica-icon' + +const features = [ + { + title: 'Your AI', + description: 'Choose your preferred model. Extend its abilities with Skills.', + }, + { + title: 'Your Machine', + description: 'Runs locally on your computer. Your data stays with you.', + }, + { + title: 'Your Control', + description: 'You set the boundaries. The AI works within them.', + }, +] + +interface WelcomeStepProps { + onStart: () => void +} + +export default function WelcomeStep({ onStart }: WelcomeStepProps) { + return ( +
+
+ {/* Brand Title */} +
+ +

+ Welcome to Multica +

+
+ + {/* Intro */} +

+ An AI assistant that gets things done — pulling data, running analysis, + and taking action. Talk to it like a team member. +

+ + {/* Feature List */} +
+

+ Built on three principles +

+ {features.map((feature) => ( +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+ + {/* CTA Button */} + +
+
+ ) +} diff --git a/apps/desktop/src/renderer/src/pages/onboarding/index.tsx b/apps/desktop/src/renderer/src/pages/onboarding/index.tsx new file mode 100644 index 00000000..2a9612f7 --- /dev/null +++ b/apps/desktop/src/renderer/src/pages/onboarding/index.tsx @@ -0,0 +1,56 @@ +import { useNavigate } from 'react-router-dom' +import { Stepper } from '../../components/onboarding/stepper' +import { useOnboardingStore } from '../../stores/onboarding' +import WelcomeStep from './components/welcome-step' +import PermissionsStep from './components/permissions-step' +import SetupStep from './components/setup-step' +import ConnectStep from './components/connect-step' +import TryItStep from './components/try-it-step' + +export default function OnboardingPage() { + const navigate = useNavigate() + const { currentStep, nextStep, prevStep, completeOnboarding } = useOnboardingStore() + + const handleComplete = () => { + completeOnboarding() + navigate('/') + } + + // Welcome step (step 0) has no stepper + if (currentStep === 0) { + return ( +
+ {/* Draggable title bar region for macOS */} +
+
+ +
+
+ ) + } + + return ( +
+ {/* Draggable title bar region for macOS + stepper */} +
+ {/* Spacer for traffic lights */} +
+ +
+ + {/* Step content */} +
+ {currentStep === 1 && } + {currentStep === 2 && } + {currentStep === 3 && } + {currentStep === 4 && } +
+
+ ) +} diff --git a/apps/desktop/src/renderer/src/pages/onboarding/layout.tsx b/apps/desktop/src/renderer/src/pages/onboarding/layout.tsx deleted file mode 100644 index eb04d8fa..00000000 --- a/apps/desktop/src/renderer/src/pages/onboarding/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Outlet, useLocation } from 'react-router-dom' -import { Stepper, type StepId } from '../../components/onboarding/stepper' - -export default function OnboardingLayout() { - const location = useLocation() - - // Derive current step from URL path - const pathSegment = location.pathname.split('/').pop() as string - const validSteps: StepId[] = ['permissions', 'setup', 'connect', 'try-it'] - const currentStep: StepId = validSteps.includes(pathSegment as StepId) - ? (pathSegment as StepId) - : 'permissions' - - return ( -
- {/* Draggable title bar region for macOS + stepper */} -
- {/* Spacer for traffic lights */} -
- -
- - {/* Step content */} -
- -
-
- ) -} diff --git a/apps/desktop/src/renderer/src/stores/onboarding.ts b/apps/desktop/src/renderer/src/stores/onboarding.ts index 3282025a..d627e7fc 100644 --- a/apps/desktop/src/renderer/src/stores/onboarding.ts +++ b/apps/desktop/src/renderer/src/stores/onboarding.ts @@ -11,10 +11,14 @@ interface AcknowledgementsState { interface OnboardingStore { completed: boolean forceOnboarding: boolean + currentStep: number acknowledgements: AcknowledgementsState allAcknowledged: boolean providerConfigured: boolean clientConnected: boolean + setStep: (step: number) => void + nextStep: () => void + prevStep: () => void setAcknowledgement: (key: keyof AcknowledgementsState, value: boolean) => void setProviderConfigured: (configured: boolean) => void setClientConnected: (connected: boolean) => void @@ -27,6 +31,7 @@ export const useOnboardingStore = create()( (set, get) => ({ completed: false, forceOnboarding: false, + currentStep: 0, acknowledgements: { fileSystem: false, @@ -38,6 +43,10 @@ export const useOnboardingStore = create()( providerConfigured: false, clientConnected: false, + setStep: (step) => set({ currentStep: step }), + nextStep: () => set({ currentStep: Math.min(get().currentStep + 1, 4) }), + prevStep: () => set({ currentStep: Math.max(get().currentStep - 1, 0) }), + setAcknowledgement: (key, value) => { const acknowledgements = { ...get().acknowledgements, [key]: value } const allAcknowledged = Object.values(acknowledgements).every(Boolean) @@ -48,7 +57,7 @@ export const useOnboardingStore = create()( setClientConnected: (connected) => set({ clientConnected: connected }), - completeOnboarding: () => set({ completed: true, forceOnboarding: false }), + completeOnboarding: () => set({ completed: true, forceOnboarding: false, currentStep: 0 }), initForceFlag: async () => { const flags = await window.electronAPI.app.getFlags() diff --git a/packages/ui/src/components/multica-icon.tsx b/packages/ui/src/components/multica-icon.tsx index cfb222bd..c5d87e30 100644 --- a/packages/ui/src/components/multica-icon.tsx +++ b/packages/ui/src/components/multica-icon.tsx @@ -1,5 +1,12 @@ import { cn } from "@multica/ui/lib/utils"; +interface MulticaIconProps extends React.ComponentProps<"span"> { + /** + * If true, play a one-time entrance spin animation (2 seconds). + */ + animate?: boolean; +} + /** * Pure CSS 8-pointed asterisk icon matching the Multica logo. * Uses currentColor so it adapts to light/dark themes automatically. @@ -7,11 +14,16 @@ import { cn } from "@multica/ui/lib/utils"; */ export function MulticaIcon({ className, + animate = false, ...props -}: React.ComponentProps<"span">) { +}: MulticaIconProps) { return (