mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 12:34:50 +02:00
[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:
@@ -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'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'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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user