[eric] onboarding: reuse real logo.png + relocate connector catalog to shared/ (downward abstraction) + reuse real INTEGRATIONS/toolsSlice/startOAuth in Connect step

This commit is contained in:
ciregenz
2026-06-30 23:36:30 -07:00
parent eb5da15dd3
commit 3f4a34092e
14 changed files with 69 additions and 101 deletions
@@ -30,7 +30,6 @@ export const OnboardingFlow: React.FC<{ onExit: () => void }> = ({ onExit }) =>
const [persona, setPersona] = useState<PersonaId | null>(null);
const [name, setName] = useState('');
const [consent, setConsent] = useState(false);
const [connectedIds, setConnectedIds] = useState<string[]>([]);
const floor = useMemo(() => demoPayoff(persona), [persona]);
// Background read-only profiling (only if they consented); upgrades the payoff in place when it lands.
@@ -77,14 +76,7 @@ export const OnboardingFlow: React.FC<{ onExit: () => void }> = ({ onExit }) =>
case 'consent':
return <PersonalizeConsent onConsent={(yes) => { setConsent(yes); setStep(yes ? 'connect' : 'payoff'); }} />;
case 'connect':
return (
<ConnectApps
connectedIds={connectedIds}
onConnect={(id) => setConnectedIds((ids) => (ids.includes(id) ? ids : [...ids, id]))}
onContinue={() => setStep('payoff')}
onSkip={() => setStep('payoff')}
/>
);
return <ConnectApps onContinue={() => setStep('payoff')} onSkip={() => setStep('payoff')} />;
case 'payoff':
return (
<Payoff
@@ -34,25 +34,3 @@ export const LineIcon: React.FC<{ name: IconName; size?: number; strokeWidth?: n
{ICON_INNER[name]}
</svg>
);
// The 12-ray coral burst; drawn, not a font glyph, so it's crisp at any size.
export const Spark: React.FC<{ size?: number; color?: string }> = ({ size = 20, color = '#D97757' }) => {
const rays = [];
for (let i = 0; i < 12; i++) {
const a = (i * 30 * Math.PI) / 180;
rays.push(
<line
key={i}
x1={12 + 3.1 * Math.cos(a)}
y1={12 + 3.1 * Math.sin(a)}
x2={12 + 9.3 * Math.cos(a)}
y2={12 + 9.3 * Math.sin(a)}
/>,
);
}
return (
<svg width={size} height={size} viewBox="0 0 24 24" stroke={color} strokeWidth={1.7} strokeLinecap="round" aria-hidden>
{rays}
</svg>
);
};
@@ -7,7 +7,6 @@ import { createPortal } from 'react-dom';
import { motion } from 'framer-motion';
import { useReducedMotion } from '@/shared/hooks/useReducedMotion';
import { useOnboardingSkin, ONBOARDING_EASE } from './onboardingSkin';
import { Spark } from './OnboardingIcons';
interface Props {
children: React.ReactNode;
@@ -34,9 +33,7 @@ export const OnboardingShell: React.FC<Props> = ({ children, stepKey }) => {
}}
>
<div style={{ position: 'fixed', top: 34, left: '50%', transform: 'translateX(-50%)', display: 'flex', alignItems: 'center', gap: 9 }}>
<span style={{ lineHeight: 0 }}>
<Spark size={20} color={S.accent} />
</span>
<img src="./logo.png" alt="OpenSwarm" style={{ width: 22, height: 22, borderRadius: 2, opacity: 0.9 }} />
<span style={{ fontFamily: S.serif, fontSize: 25, fontWeight: 500, letterSpacing: 0.2 }}>OpenSwarm</span>
</div>
@@ -18,13 +18,6 @@ export interface PersonaOption {
export type FlowStepId = 'help' | 'name' | 'consent' | 'connect' | 'payoff';
export interface ConnectorOption {
id: string;
name: string;
description: string;
icon: IconName;
}
export interface PayoffIdea {
id: string;
icon: IconName;
@@ -1,46 +1,45 @@
// D4: optional connect. The connectors the profiling agent may later read. Skippable; payoff still works.
// UI-only here: real OAuth (integrations.tsx / POST /{tool_id}/oauth/start) wires in a follow step.
// D4: optional connect. Reuses the REAL shared connector catalog (icons + metadata), the REAL
// connection state from toolsSlice, and the REAL startOAuth thunk. No hardcoded list, no fake toggle.
import React from 'react';
import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { INTEGRATIONS, Integration } from '@/shared/integrations/catalog';
import { fetchTools, startOAuth, ToolDefinition } from '@/shared/state/toolsSlice';
import { useOnboardingSkin } from '../onboardingSkin';
import { LineIcon } from '../OnboardingIcons';
import { Heading, Sub, PrimaryButton, GhostLink } from '../OnboardingAtoms';
import type { ConnectorOption } from '../onboardingFlowTypes';
const CONNECTORS: ConnectorOption[] = [
{ id: 'google', name: 'Google', description: 'Gmail, Calendar, Drive', icon: 'mail' },
{ id: 'notion', name: 'Notion', description: 'notes, docs, tasks', icon: 'doc' },
{ id: 'slack', name: 'Slack', description: 'messages, channels', icon: 'chat' },
];
// The connectors most useful for the profiling payoff; drawn from the real catalog by id.
const SHOWN_IDS = ['google-workspace', 'notion', 'slack'];
const Row: React.FC<{ option: ConnectorOption; connected: boolean; onConnect: () => void }> = ({
option,
connected,
onConnect,
}) => {
function toolFor(integration: Integration, items: Record<string, ToolDefinition>): ToolDefinition | undefined {
return Object.values(items).find((t) => t.name === integration.name);
}
const Row: React.FC<{ integration: Integration }> = ({ integration }) => {
const S = useOnboardingSkin();
const dispatch = useAppDispatch();
const items = useAppSelector((s) => s.tools.items);
const tool = toolFor(integration, items);
const connected = tool?.auth_status === 'connected';
const connect = async () => {
try {
const res = await dispatch(startOAuth(tool?.id ?? integration.id)).unwrap();
if (res.auth_url) window.open(res.auth_url, '_blank');
} catch { /* connect failed; leave as-is */ }
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 14,
background: S.surface,
border: `1px solid ${S.border}`,
borderRadius: 14,
padding: '16px 18px',
textAlign: 'left',
}}
>
<span style={{ color: S.text, opacity: 0.9, display: 'flex' }}>
<LineIcon name={option.icon} size={20} />
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, background: S.surface, border: `1px solid ${S.border}`, borderRadius: 14, padding: '16px 18px', textAlign: 'left' }}>
<span style={{ display: 'flex', width: 22, justifyContent: 'center' }}>{integration.icon}</span>
<div>
<div style={{ fontSize: 15, fontWeight: 500 }}>{option.name}</div>
<div style={{ fontSize: 12.5, color: S.muted, marginTop: 2 }}>{option.description}</div>
<div style={{ fontSize: 15, fontWeight: 500 }}>{integration.name}</div>
<div style={{ fontSize: 12.5, color: S.muted, marginTop: 2 }}>
{connected && tool?.connected_account_email ? tool.connected_account_email : integration.description}
</div>
</div>
<span
onClick={connected ? undefined : onConnect}
onClick={connected ? undefined : connect}
style={{
marginLeft: 'auto',
fontSize: 13,
@@ -50,6 +49,7 @@ const Row: React.FC<{ option: ConnectorOption; connected: boolean; onConnect: ()
borderRadius: 999,
padding: '6px 15px',
cursor: connected ? 'default' : 'pointer',
whiteSpace: 'nowrap',
}}
>
{connected ? 'Connected' : 'Connect'}
@@ -58,21 +58,29 @@ const Row: React.FC<{ option: ConnectorOption; connected: boolean; onConnect: ()
);
};
export const ConnectApps: React.FC<{
connectedIds: string[];
onConnect: (id: string) => void;
onContinue: () => void;
onSkip: () => void;
}> = ({ connectedIds, onConnect, onContinue, onSkip }) => (
<>
<Heading>Connect what I can work with</Heading>
<Sub>optional, and I&#39;ll only ever read it to understand you</Sub>
<div style={{ marginTop: 44, width: '100%', maxWidth: 440, display: 'flex', flexDirection: 'column', gap: 11 }}>
{CONNECTORS.map((c) => (
<Row key={c.id} option={c} connected={connectedIds.includes(c.id)} onConnect={() => onConnect(c.id)} />
))}
</div>
<PrimaryButton onClick={onContinue} style={{ maxWidth: 200 }}>Continue</PrimaryButton>
<GhostLink onClick={onSkip}>skip for now</GhostLink>
</>
);
export const ConnectApps: React.FC<{ onContinue: () => void; onSkip: () => void }> = ({ onContinue, onSkip }) => {
const dispatch = useAppDispatch();
const shown = INTEGRATIONS.filter((i) => SHOWN_IDS.includes(i.id));
// Load real connection state, and re-check when the user returns from the OAuth browser tab.
useEffect(() => {
dispatch(fetchTools());
const onFocus = () => dispatch(fetchTools());
window.addEventListener('focus', onFocus);
return () => window.removeEventListener('focus', onFocus);
}, [dispatch]);
return (
<>
<Heading>Connect what I can work with</Heading>
<Sub>optional, and I&#39;ll only ever read it to understand you</Sub>
<div style={{ marginTop: 44, width: '100%', maxWidth: 440, display: 'flex', flexDirection: 'column', gap: 11 }}>
{shown.map((i) => (
<Row key={i.id} integration={i} />
))}
</div>
<PrimaryButton onClick={onContinue} style={{ maxWidth: 200 }}>Continue</PrimaryButton>
<GhostLink onClick={onSkip}>skip for now</GhostLink>
</>
);
};
+1 -1
View File
@@ -29,7 +29,7 @@ import {
import { Skeleton } from '@/app/components/feedback/Loading';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration, INTEGRATIONS } from './integrations';
import { Integration, INTEGRATIONS } from '@/shared/integrations/catalog';
import { CATEGORY_ORDER } from './toolsHelpers';
import ToolSection from './cards/ToolSection';
import BrowserPermissionCard from './cards/BrowserPermissionCard';
@@ -23,7 +23,7 @@ import SecurityIcon from '@mui/icons-material/Security';
import RefreshIcon from '@mui/icons-material/Refresh';
import { ToolDefinition } from '@/shared/state/toolsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration } from '../integrations';
import { Integration } from '@/shared/integrations/catalog';
import ServiceGroup from './ServiceGroup';
import CustomToolDevInfo from './CustomToolDevInfo';
import CustomToolConnect from './CustomToolConnect';
@@ -6,7 +6,7 @@ import LinkIcon from '@mui/icons-material/Link';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { ToolDefinition } from '@/shared/state/toolsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration } from '../integrations';
import { Integration } from '@/shared/integrations/catalog';
interface CustomToolConnectProps {
tool: ToolDefinition;
@@ -8,7 +8,7 @@ import CircularProgress from '@mui/material/CircularProgress';
import Switch from '@mui/material/Switch';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration } from '../integrations';
import { Integration } from '@/shared/integrations/catalog';
interface IntegrationGalleryCardProps {
integration: Integration;
@@ -13,7 +13,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
import PanToolIcon from '@mui/icons-material/PanTool';
import { ToolDefinition } from '@/shared/state/toolsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration } from '../integrations';
import { Integration } from '@/shared/integrations/catalog';
const toDisplayName = (name: string, serviceName?: string) => {
let display = name.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
@@ -12,7 +12,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import LinkIcon from '@mui/icons-material/Link';
import { McpServer } from '@/shared/state/mcpRegistrySlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration } from '../integrations';
import { Integration } from '@/shared/integrations/catalog';
import { ToolForm } from '../toolsHelpers';
import McpConfigDialog from './McpConfigDialog';
@@ -11,7 +11,7 @@ import {
ToolDefinition,
} from '@/shared/state/toolsSlice';
import { API_BASE } from '@/shared/config';
import { Integration } from '../integrations';
import { Integration } from '@/shared/integrations/catalog';
type Snackbar = { open: boolean; message: string; severity?: 'success' | 'error' };
@@ -11,7 +11,7 @@ import {
} from '@/shared/state/toolsSlice';
import { McpServer } from '@/shared/state/mcpRegistrySlice';
import { ToolForm, emptyForm } from '../toolsHelpers';
import { Integration } from '../integrations';
import { Integration } from '@/shared/integrations/catalog';
import { useToolConnections } from './useToolConnections';
import { useRegistryBrowser } from './useRegistryBrowser';