mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-30 13:34:50 +02:00
[eric] onboarding: minimalism pass (arc radio rows, one-line copy, no icon spam, clean light stage)
This commit is contained in:
@@ -183,7 +183,7 @@ const OnboardingV3Root: React.FC = () => {
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
{beat === 'welcome' && <IntroBeat c={c} line="Welcome." onNext={() => setBeat('newos')} />}
|
||||
{beat === 'newos' && <IntroBeat c={c} line="This is your new OS." sub="A canvas where AI agents do real work for you." onNext={() => setBeat('connect')} />}
|
||||
{beat === 'newos' && <IntroBeat c={c} line="This is your new OS." onNext={() => setBeat('connect')} />}
|
||||
{beat === 'connect' && (
|
||||
<BeatConnect
|
||||
c={c}
|
||||
|
||||
@@ -28,8 +28,8 @@ const BeatApps: React.FC<{
|
||||
<BeatShell
|
||||
c={c}
|
||||
title="Choose the apps you live in."
|
||||
body="I'll shape your starting canvas around them, and I can connect to them later so your agents work where you already do."
|
||||
nextLabel={picks.length > 0 ? 'Continue' : 'Skip for now'}
|
||||
body="I'll shape your starting canvas around them."
|
||||
nextLabel={picks.length > 0 ? 'Next' : 'Skip for now'}
|
||||
onNext={onNext}
|
||||
onBack={onBack}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,7 @@ const BeatCard: React.FC<{
|
||||
<BeatShell
|
||||
c={c}
|
||||
title={name ? `Welcome to OpenSwarm, ${name}.` : 'Welcome to OpenSwarm.'}
|
||||
body="Here's your Swarm Card. And with that, your canvas is ready."
|
||||
body="Here's your Swarm Card. Show it off or keep it to yourself."
|
||||
nextLabel="Get started"
|
||||
onNext={() => onFinish(name.trim() || null)}
|
||||
onBack={onBack}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import { fetchModels } from '@/shared/state/modelsSlice';
|
||||
@@ -13,7 +12,7 @@ import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
import type { ProviderIdentity } from '../onboardingV3Api';
|
||||
import BeatShell from './BeatShell';
|
||||
|
||||
// The single ask of the whole flow. Reuses the proven Settings connect flow verbatim; the scan disclosure lives here as one honest line with an opt-out, and the scan runs during the OAuth wait.
|
||||
// The single ask of the whole flow, staged as Arc's import list: radio rows, no filler copy. Reuses the proven Settings connect flow verbatim; the scan disclosure is one quiet line, and the scan runs during the OAuth wait.
|
||||
const BeatConnect: React.FC<{
|
||||
c: ClaudeTokens;
|
||||
identity: ProviderIdentity[];
|
||||
@@ -82,61 +81,64 @@ const BeatConnect: React.FC<{
|
||||
<BeatShell
|
||||
c={c}
|
||||
title="Connect your AI."
|
||||
body="Use the subscription you already pay for. OpenSwarm runs on your own Claude, ChatGPT, or Gemini account, right from this Mac."
|
||||
nextLabel={connected ? 'Continue' : 'Continue without connecting'}
|
||||
body="Use the subscription you already pay for."
|
||||
nextLabel="Continue"
|
||||
onNext={onNext}
|
||||
onBack={onBack}
|
||||
>
|
||||
<div style={{ width: 'min(460px, 100%)', display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{SUBSCRIPTION_PROVIDERS.map((p, i) => (
|
||||
<motion.button
|
||||
key={p.id}
|
||||
onClick={() => !connected && handleConnect(p.id)}
|
||||
initial={{ opacity: 0, y: 14 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 320, damping: 26, delay: 0.1 + i * 0.08 }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14, padding: '16px 18px', textAlign: 'left',
|
||||
borderRadius: c.radius.md, border: `1px solid ${c.border.medium}`, background: c.bg.surface,
|
||||
cursor: connected ? 'default' : 'pointer', fontFamily: 'inherit',
|
||||
boxShadow: c.shadow.sm,
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 12, height: 12, borderRadius: 999, background: p.color, flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{ display: 'block', fontSize: '1rem', fontWeight: 600, color: c.text.primary }}>{p.name}</span>
|
||||
<span style={{ display: 'block', fontSize: '0.82rem', color: c.text.tertiary }}>{p.desc}</span>
|
||||
</span>
|
||||
{connecting === p.id && !connected && <span style={{ fontSize: '0.8rem', color: c.text.tertiary }}>waiting for sign-in...</span>}
|
||||
{connected && connecting === p.id && <Check size={18} color={c.status.success} />}
|
||||
</motion.button>
|
||||
))}
|
||||
<div style={{ width: 'min(440px, 100%)', display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{SUBSCRIPTION_PROVIDERS.map((p, i) => {
|
||||
const isThis = connecting === p.id;
|
||||
const filled = connected && isThis;
|
||||
return (
|
||||
<motion.button
|
||||
key={p.id}
|
||||
onClick={() => !connected && handleConnect(p.id)}
|
||||
initial={{ opacity: 0, y: 14 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 320, damping: 26, delay: 0.1 + i * 0.08 }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14, padding: '13px 14px 13px 16px', textAlign: 'left',
|
||||
borderRadius: c.radius.md, border: `1px solid ${filled ? c.accent.primary : c.border.medium}`,
|
||||
background: c.bg.surface, cursor: connected ? 'default' : 'pointer', fontFamily: 'inherit',
|
||||
boxShadow: c.shadow.sm,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 18, height: 18, borderRadius: 999, flexShrink: 0, boxSizing: 'border-box',
|
||||
border: `2px solid ${filled ? c.accent.primary : c.border.strong}`,
|
||||
background: filled ? c.accent.primary : 'transparent',
|
||||
boxShadow: filled ? `inset 0 0 0 3px ${c.bg.surface}` : 'none',
|
||||
}} />
|
||||
<span style={{ flex: 1, fontSize: '1rem', fontWeight: 600, color: c.text.primary }}>
|
||||
{p.name}
|
||||
{isThis && !connected && <span style={{ marginLeft: 10, fontSize: '0.8rem', fontWeight: 400, color: c.text.tertiary }}>waiting for sign-in...</span>}
|
||||
</span>
|
||||
<span style={{
|
||||
width: 36, height: 36, borderRadius: 9, flexShrink: 0, background: `${p.color}26`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<span style={{ width: 13, height: 13, borderRadius: 999, background: p.color }} />
|
||||
</span>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
{userCode && !connected && (
|
||||
<div style={{ textAlign: 'center', padding: '10px 0', fontSize: '0.9rem', color: c.text.secondary }}>
|
||||
<div style={{ textAlign: 'center', padding: '6px 0', fontSize: '0.9rem', color: c.text.secondary }}>
|
||||
Your code: <strong style={{ fontFamily: c.font.mono, letterSpacing: '0.08em' }}>{userCode}</strong>
|
||||
</div>
|
||||
)}
|
||||
{connected && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
style={{
|
||||
padding: '12px 16px', borderRadius: c.radius.md, background: c.status.successBg,
|
||||
color: c.status.success, fontSize: '0.9rem', fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} style={{ padding: '4px 2px', fontSize: '0.9rem', fontWeight: 500, color: c.status.success }}>
|
||||
Connected{connectedIdentity?.email ? ` as ${connectedIdentity.email}` : ''}
|
||||
{connectedIdentity?.plan ? ` · ${connectedIdentity.label} ${connectedIdentity.plan}` : ''}
|
||||
</motion.div>
|
||||
)}
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 9, marginTop: 6, cursor: 'pointer', fontSize: '0.8rem', color: c.text.tertiary, lineHeight: 1.5 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 9, marginTop: 8, cursor: 'pointer', fontSize: '0.82rem', color: c.text.tertiary, lineHeight: 1.5 }}>
|
||||
<input type="checkbox" checked={scanConsent} onChange={(e) => setScanConsent(e.target.checked)} style={{ marginTop: 2 }} />
|
||||
<span>
|
||||
While you sign in, take a quick local look around (app names and folder counts only) to personalize my suggestions.
|
||||
Nothing is stored or sent anywhere except to your own AI.
|
||||
</span>
|
||||
<span>Take a quick local look around to personalize my setup. Nothing leaves this Mac.</span>
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: 18, marginTop: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 18, marginTop: 2 }}>
|
||||
<button onClick={() => setShowKeys(!showKeys)} style={{ border: 'none', background: 'transparent', padding: 0, color: c.text.ghost, fontSize: '0.82rem', cursor: 'pointer', fontFamily: 'inherit', textDecoration: 'underline' }}>
|
||||
Use an API key instead
|
||||
</button>
|
||||
|
||||
@@ -44,7 +44,7 @@ const BeatShell: React.FC<{
|
||||
background: c.bg.inverse, color: c.text.inverse, clipPath: ZIGZAG_CLIP,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: GRAIN_URL, opacity: 0.16, pointerEvents: 'none', mixBlendMode: 'overlay' }} />
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: GRAIN_URL, opacity: 0.1, pointerEvents: 'none', mixBlendMode: 'overlay' }} />
|
||||
{onBack && (
|
||||
<motion.button
|
||||
{...enter(0.05)}
|
||||
@@ -89,7 +89,6 @@ const BeatShell: React.FC<{
|
||||
padding: 36, boxSizing: 'border-box', overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: GRAIN_URL, opacity: 0.07, pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'relative', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -24,8 +24,8 @@ const BeatTheme: React.FC<{
|
||||
<BeatShell
|
||||
c={c}
|
||||
title="Make it yours."
|
||||
body="Pick a color, or add a second dot for a gradient. The whole app repaints as you drag; this is your home now."
|
||||
nextLabel="Continue"
|
||||
body="The whole app repaints as you drag. Add a second dot for a gradient."
|
||||
nextLabel="Next"
|
||||
onNext={onNext}
|
||||
onBack={onBack}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ArrowLeft, CalendarClock, Check, Sparkles } from 'lucide-react';
|
||||
import { ArrowLeft, CalendarClock, Check } from 'lucide-react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { createWorkflow } from '@/shared/state/workflowsSlice';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
@@ -87,7 +87,6 @@ const WelcomeQuickReplies: React.FC<{
|
||||
cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
<Sparkles size={14} color={c.accent.primary} style={{ flexShrink: 0 }} />
|
||||
{s.title}
|
||||
</motion.button>
|
||||
))}
|
||||
|
||||
@@ -34,7 +34,6 @@ export function useOnboardingRevealSeed({ isActive, canvasEmpty, viewportRef, ca
|
||||
const lines: string[] = ['While you were setting up, I got some ideas ready.'];
|
||||
if (scanSummary) lines.push(`Spotted on this Mac: ${scanSummary}.`);
|
||||
if (starters.length > 0) lines.push(`Ready to run:\n${starters.map((s) => `- ${s.title}`).join('\n')}`);
|
||||
lines.push('Pick one in the chat, or just type what you need.');
|
||||
dispatch(addNote({ x: cx + DEFAULT_CARD_W / 2 + 48, y: cy - 140, color: 'yellow', content: lines.join('\n\n') }));
|
||||
}
|
||||
createWelcomeDraft();
|
||||
|
||||
Reference in New Issue
Block a user