'use client' import { useState, useCallback } from 'react' import type { FlowNode, IntegrationConfig } from '@/lib/types' import { SUPPORTED_CHAINS, getBalances, detectSafeChains } from '@/lib/api/safe-client' import type { SafeBalance, DetectedChain } from '@/lib/api/safe-client' import { safeBalancesToFunnels, proposalsToOutcomes } from '@/lib/integrations' import { fetchSpace, fetchPassedProposals } from '@/lib/api/rvote-client' import type { RVoteProposal } from '@/lib/api/rvote-client' interface IntegrationPanelProps { isOpen: boolean onClose: () => void onImportNodes: (nodes: FlowNode[]) => void integrations?: IntegrationConfig onIntegrationsChange: (config: IntegrationConfig) => void } export default function IntegrationPanel({ isOpen, onClose, onImportNodes, integrations, onIntegrationsChange, }: IntegrationPanelProps) { // Safe wallet state const [safeAddress, setSafeAddress] = useState(integrations?.safe?.address || '') const [selectedChains, setSelectedChains] = useState( integrations?.safe?.chainIds || [100, 10] ) const [detectedChains, setDetectedChains] = useState([]) const [balances, setBalances] = useState>(new Map()) const [safeFetching, setSafeFetching] = useState(false) const [safeError, setSafeError] = useState('') // rVote state const [rvoteSlug, setRvoteSlug] = useState(integrations?.rvote?.spaceSlug || '') const [rvoteFetching, setRvoteFetching] = useState(false) const [rvoteError, setRvoteError] = useState('') const [rvoteSpace, setRvoteSpace] = useState<{ name: string; memberCount: number } | null>(null) const [rvoteProposals, setRvoteProposals] = useState>([]) const [fetchedProposals, setFetchedProposals] = useState([]) const handleFetchSafe = useCallback(async () => { if (!safeAddress.match(/^0x[a-fA-F0-9]{40}$/)) { setSafeError('Invalid Ethereum address') return } setSafeFetching(true) setSafeError('') setBalances(new Map()) try { const detected = await detectSafeChains(safeAddress, selectedChains) setDetectedChains(detected) if (detected.length === 0) { setSafeError('No Safe found on selected chains') setSafeFetching(false) return } const balanceMap = new Map() for (const { chainId } of detected) { const chainBalances = await getBalances(safeAddress, chainId) balanceMap.set(chainId, chainBalances) } setBalances(balanceMap) onIntegrationsChange({ ...integrations, safe: { address: safeAddress, chainIds: detected.map((d) => d.chainId), lastSyncedAt: Date.now(), }, }) } catch (e) { setSafeError(e instanceof Error ? e.message : 'Failed to fetch') } finally { setSafeFetching(false) } }, [safeAddress, selectedChains, integrations, onIntegrationsChange]) const handleImportSafe = useCallback(() => { const allNodes: FlowNode[] = [] let xOffset = 0 balances.forEach((chainBalances, chainId) => { const nodes = safeBalancesToFunnels(chainBalances, safeAddress, chainId, undefined, { x: xOffset, y: 100, }) allNodes.push(...nodes) xOffset += nodes.length * 280 }) if (allNodes.length > 0) { onImportNodes(allNodes) onClose() } }, [balances, safeAddress, onImportNodes, onClose]) const handleFetchRvote = useCallback(async () => { if (!rvoteSlug.trim()) { setRvoteError('Enter a space slug') return } setRvoteFetching(true) setRvoteError('') try { const space = await fetchSpace(rvoteSlug) setRvoteSpace({ name: space.name, memberCount: space._count?.members || 0 }) const proposals = await fetchPassedProposals(rvoteSlug) setRvoteProposals( proposals.map((p) => ({ id: p.id, title: p.title, score: p.score })) ) setFetchedProposals(proposals) onIntegrationsChange({ ...integrations, rvote: { spaceSlug: rvoteSlug, spaceName: space.name, lastSyncedAt: Date.now(), }, }) } catch (e) { setRvoteError(e instanceof Error ? e.message : 'Failed to fetch') } finally { setRvoteFetching(false) } }, [rvoteSlug, integrations, onIntegrationsChange]) const handleImportRvote = useCallback(() => { if (fetchedProposals.length === 0) return const nodes = proposalsToOutcomes(fetchedProposals, rvoteSlug) if (nodes.length > 0) { onImportNodes(nodes) onClose() } }, [fetchedProposals, rvoteSlug, onImportNodes, onClose]) const toggleChain = (chainId: number) => { setSelectedChains((prev) => prev.includes(chainId) ? prev.filter((c) => c !== chainId) : [...prev, chainId] ) } if (!isOpen) return null const totalBalances = Array.from(balances.values()).flat().filter((b) => parseFloat(b.fiatBalance) > 1) return (

Link External Data

{/* ─── Safe Wallet Section ─── */}

Safe Wallet → Funnels

setSafeAddress(e.target.value)} placeholder="0x... Safe address" className="w-full px-3 py-2 rounded-lg border border-slate-300 text-sm font-mono focus:outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400" />
{Object.entries(SUPPORTED_CHAINS).map(([id, chain]) => ( ))}
{safeError &&

{safeError}

} {detectedChains.length > 0 && (

Safe found on{' '} {detectedChains.map((d) => d.chain.name).join(', ')} {' '}({detectedChains[0]?.safeInfo.owners.length} owners, {detectedChains[0]?.safeInfo.threshold} threshold)

{totalBalances.length > 0 && ( <>
{totalBalances.slice(0, 8).map((b, i) => (
{b.symbol} {b.balanceFormatted} (${parseFloat(b.fiatBalance).toLocaleString(undefined, { maximumFractionDigits: 0 })})
))} {totalBalances.length > 8 && (

+{totalBalances.length - 8} more tokens

)}
)}
)}
{/* ─── rVote Section ─── */}

rVote Space → Outcomes

setRvoteSlug(e.target.value)} placeholder="Space slug (e.g., crypto)" className="flex-1 px-3 py-2 rounded-lg border border-slate-300 text-sm focus:outline-none focus:border-violet-400 focus:ring-1 focus:ring-violet-400" /> .rvote.online
{rvoteError &&

{rvoteError}

} {rvoteSpace && (

{rvoteSpace.name} {' '}• {rvoteSpace.memberCount} members {' '}• {rvoteProposals.length} passed proposal{rvoteProposals.length !== 1 ? 's' : ''}

{rvoteProposals.length > 0 ? ( <>
{rvoteProposals.slice(0, 6).map((p) => (
{p.title} +{p.score}
))} {rvoteProposals.length > 6 && (

+{rvoteProposals.length - 6} more

)}
) : (

No passed proposals found in this space

)}
)}
) }