aibanking.dev / views /Flows.tsx
admin08077's picture
Update views/Flows.tsx
3c43d55 verified
import React, { useState, useRef, useEffect } from 'react';
import {
Zap,
ArrowRightLeft,
UserPlus,
Search,
MoreHorizontal,
Clock,
CheckCircle2,
XCircle,
Copy,
Terminal,
ExternalLink,
Code,
X,
Loader2,
Cpu,
ShieldCheck,
Key,
Plus,
MessageSquare,
ArrowRight,
RefreshCw,
Eye,
Trash2,
Lock,
Globe
} from 'lucide-react';
import { AccountCollectionFlow, PaymentFlow } from '../types/index.ts';
import { callGemini } from '../services/geminiService.ts';
const BRIDGE_URL = "https://admin08077-ai-banking.static.hf.space";
interface ApiKey {
id: string;
key: string;
name: string;
scope: string;
created_at: string;
status: 'ACTIVE' | 'REVOKED';
}
const INITIAL_ACCOUNT_FLOWS: AccountCollectionFlow[] = [
{
id: 'ACF-1102',
object: 'account_collection_flow',
live_mode: true,
created_at: '2024-03-20T10:00:00Z',
updated_at: '2024-03-31T14:00:00Z',
client_token: 'acf_tok_882910398291',
status: 'pending',
counterparty_id: 'CP-8801',
external_account_id: null,
payment_types: ['ach', 'wire']
}
];
const INITIAL_PAYMENT_FLOWS: PaymentFlow[] = [
{
id: 'PF-4421',
object: 'payment_flow',
live_mode: true,
created_at: '2024-03-25T12:00:00Z',
updated_at: '2024-03-25T12:30:00Z',
client_token: 'pf_tok_4429188',
status: 'pending',
amount: 500000,
currency: 'USD',
direction: 'debit',
counterparty_id: 'CP-8801',
receiving_account_id: null,
originating_account_id: 'IA-4401'
}
];
const Flows: React.FC = () => {
const [activeTab, setActiveTab] = useState<'account' | 'payment' | 'keys'>('account');
const [accountFlows, setAccountFlows] = useState<AccountCollectionFlow[]>(INITIAL_ACCOUNT_FLOWS);
const [paymentFlows, setPaymentFlows] = useState<PaymentFlow[]>(INITIAL_PAYMENT_FLOWS);
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [isSdkModalOpen, setIsSdkModalOpen] = useState(false);
const [isInitModalOpen, setIsInitModalOpen] = useState(false);
const [selectedFlow, setSelectedFlow] = useState<any>(null);
// AI SDK Architect State
const [sdkChat, setSdkChat] = useState<{ role: 'ai' | 'user', text: string }[]>([
{ role: 'ai', text: `LQI Neural Architect active. I see your bridge is live at ${BRIDGE_URL}. I can generate the Node.js or Python code required to securely trigger RTP payments through your bridge. What environment are you connecting from?` }
]);
const [sdkInput, setSdkInput] = useState('');
const [isAiThinking, setIsAiThinking] = useState(false);
const [finalCode, setFinalCode] = useState<string | null>(null);
// Init Form State
const [initType, setInitType] = useState<'account' | 'payment'>('account');
const [initLoading, setInitLoading] = useState(false);
const [initForm, setInitForm] = useState({ counterpartyId: 'CP-8801', amount: '0', currency: 'USD' });
// API Key State
const [newKeyName, setNewKeyName] = useState('');
const [generatingKey, setGeneratingKey] = useState(false);
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const handleInitializeFlow = async () => {
setInitLoading(true);
await new Promise(r => setTimeout(r, 1500));
const id = initType === 'account' ? `ACF-${Math.floor(1000 + Math.random() * 9000)}` : `PF-${Math.floor(1000 + Math.random() * 9000)}`;
const newFlow = {
id,
object: initType === 'account' ? 'account_collection_flow' : 'payment_flow',
live_mode: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
client_token: `${initType === 'account' ? 'acf' : 'pf'}_tok_${Math.random().toString(36).substring(7)}`,
status: 'pending' as const,
counterparty_id: initForm.counterpartyId,
...(initType === 'payment' ? {
amount: parseInt(initForm.amount) * 100,
currency: initForm.currency,
direction: 'debit',
receiving_account_id: null,
originating_account_id: 'IA-4401'
} : {
external_account_id: null,
payment_types: ['ach', 'wire']
})
};
if (initType === 'account') {
setAccountFlows([newFlow as any, ...accountFlows]);
} else {
setPaymentFlows([newFlow as any, ...paymentFlows]);
}
setInitLoading(false);
setIsInitModalOpen(false);
};
const handleSdkChat = async () => {
if (!sdkInput.trim() || isAiThinking) return;
const userMsg = sdkInput;
setSdkChat(prev => [...prev, { role: 'user', text: userMsg }]);
setSdkInput('');
setIsAiThinking(true);
try {
const prompt = `
You are the Lumina Quantum Neural Architect.
Context: The user has a Node.js bridge hosted at ${BRIDGE_URL}.
The bridge endpoint is /webhooks/trigger-payment.
It expects HMAC-SHA256 signatures and a body with: amount, currency, originating_account_id, receiving_account_id, reference_number.
Chat History: ${JSON.stringify(sdkChat)}
User Instruction: ${userMsg}
Task:
1. If you need more info (e.g. language or auth secret), ask a short, sharp technical question.
2. If you have enough info, generate a full implementation snippet to talk to that bridge.
Return JSON format: { "reply": "A brief technical explanation of what you are building", "code": "The full code block or null if still asking questions" }
`;
const response = await callGemini(
'gemini-3-flash-preview',
prompt,
{ responseMimeType: "application/json" }
);
// Fix: Directly access the .text property from the response object as per guidelines
const data = JSON.parse(response.text || '{}');
setSdkChat(prev => [...prev, { role: 'ai', text: data.reply }]);
if (data.code) setFinalCode(data.code);
} catch (err) {
setSdkChat(prev => [...prev, { role: 'ai', text: "Handshake sync failed. Please re-specify parameters." }]);
} finally {
setIsAiThinking(false);
}
};
const generateApiKey = async () => {
if (!newKeyName) return;
setGeneratingKey(true);
await new Promise(r => setTimeout(r, 1000));
const key: ApiKey = {
id: `key_${Math.random().toString(36).substring(7)}`,
key: `lq_live_${Math.random().toString(36).substring(2, 15)}`,
name: newKeyName,
scope: 'registry.read write.payments',
created_at: new Date().toISOString(),
status: 'ACTIVE'
};
setApiKeys([key, ...apiKeys]);
setNewKeyName('');
setGeneratingKey(false);
};
const revokeKey = (id: string) => {
setApiKeys(apiKeys.map(k => k.id === id ? { ...k, status: 'REVOKED' } : k));
};
return (
<div className="space-y-10 animate-in fade-in duration-700">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div>
<h2 className="text-3xl font-black text-white italic tracking-tighter uppercase mb-2">Onboarding <span className="text-blue-500 not-italic">Flows</span></h2>
<p className="text-zinc-500 text-[10px] font-black uppercase tracking-[0.3em]">Institutional M2M Integration Protocols</p>
</div>
<div className="flex gap-4">
<button
onClick={() => setIsSdkModalOpen(true)}
className="flex items-center space-x-2 px-6 py-3 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all"
>
<Code size={14} />
<span>SDK Architect</span>
</button>
<button
onClick={() => setIsInitModalOpen(true)}
className="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all shadow-xl shadow-blue-900/30"
>
<Zap size={14} />
<span>Initialize Flow</span>
</button>
</div>
</div>
<div className="bg-zinc-950 border border-zinc-900 rounded-[3rem] overflow-hidden shadow-2xl">
<div className="flex border-b border-zinc-900 bg-black/20">
<button
onClick={() => setActiveTab('account')}
className={`flex-1 py-6 px-10 text-[10px] font-black uppercase tracking-[0.3em] flex items-center justify-center gap-3 transition-all ${
activeTab === 'account' ? 'text-blue-500 border-b-2 border-blue-500 bg-blue-500/5' : 'text-zinc-600 hover:text-zinc-400'
}`}
>
<UserPlus size={16} />
Account Collection
</button>
<button
onClick={() => setActiveTab('payment')}
className={`flex-1 py-6 px-10 text-[10px] font-black uppercase tracking-[0.3em] flex items-center justify-center gap-3 transition-all ${
activeTab === 'payment' ? 'text-blue-500 border-b-2 border-blue-500 bg-blue-500/5' : 'text-zinc-600 hover:text-zinc-400'
}`}
>
<ArrowRightLeft size={16} />
Payment Onboarding
</button>
<button
onClick={() => setActiveTab('keys')}
className={`flex-1 py-6 px-10 text-[10px] font-black uppercase tracking-[0.3em] flex items-center justify-center gap-3 transition-all ${
activeTab === 'keys' ? 'text-emerald-500 border-b-2 border-emerald-500 bg-emerald-500/5' : 'text-zinc-600 hover:text-zinc-400'
}`}
>
<Key size={16} />
Node Access Keys
</button>
</div>
<div className="p-10 min-h-[400px]">
{activeTab === 'keys' ? (
<div className="space-y-8">
<div className="bg-black/40 border border-zinc-800 rounded-[2rem] p-8">
<p className="text-[10px] font-black text-zinc-500 uppercase tracking-widest mb-6">Issue Secure Access Token</p>
<div className="flex gap-4">
<input
value={newKeyName}
onChange={e => setNewKeyName(e.target.value)}
placeholder="External App Name"
className="flex-1 bg-black border border-zinc-800 rounded-xl px-6 py-4 text-white text-xs outline-none focus:border-emerald-500/50"
/>
<button onClick={generateApiKey} disabled={generatingKey} className="px-8 bg-emerald-600 hover:bg-emerald-500 text-white rounded-xl font-black text-[10px] uppercase tracking-widest transition-all">
{generatingKey ? <Loader2 size={16} className="animate-spin" /> : 'Issue Token'}
</button>
</div>
</div>
<div className="space-y-4">
{apiKeys.map(k => (
<div key={k.id} className="bg-zinc-900 border border-zinc-800 p-6 rounded-2xl flex justify-between items-center group transition-all">
<div className="flex items-center gap-6">
<div className={`p-4 rounded-xl ${k.status === 'ACTIVE' ? 'bg-emerald-500/10 text-emerald-500' : 'bg-zinc-800 text-zinc-600'}`}>
<Key size={18} />
</div>
<div>
<p className="text-white font-black text-xs uppercase italic">{k.name}</p>
<p className="text-[9px] text-zinc-600 font-mono mt-1">{k.key}</p>
</div>
</div>
<div className="flex items-center gap-4">
{k.status === 'ACTIVE' && <button onClick={() => revokeKey(k.id)} className="p-2 text-rose-500 hover:bg-rose-500/10 rounded-lg transition-colors"><Trash2 size={16} /></button>}
<div className={`px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest ${k.status === 'ACTIVE' ? 'bg-emerald-500/10 text-emerald-500' : 'bg-rose-500/10 text-rose-500'}`}>{k.status}</div>
</div>
</div>
))}
</div>
</div>
) : (
<div className="space-y-4">
{(activeTab === 'account' ? accountFlows : paymentFlows).map((flow: any) => (
<div key={flow.id} className="bg-black/40 border border-zinc-900 rounded-[2rem] p-8 group hover:border-zinc-700 transition-all flex flex-col lg:flex-row justify-between items-center gap-8">
<div className="flex items-center gap-6">
<div className="w-14 h-14 bg-zinc-900 border border-zinc-800 rounded-2xl flex items-center justify-center text-zinc-500 group-hover:text-blue-500 transition-all">
{activeTab === 'account' ? <UserPlus size={24} /> : <ArrowRightLeft size={24} />}
</div>
<div>
<div className="flex items-center gap-3 mb-1">
<p className="text-white font-black text-sm uppercase italic">{flow.id}</p>
<StatusBadge status={flow.status} />
</div>
<p className="text-[10px] text-zinc-600 font-bold uppercase tracking-widest">CP_REF: {flow.counterparty_id}</p>
</div>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => setSelectedFlow(flow)}
className="px-8 py-3 bg-zinc-900 hover:bg-white hover:text-black text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border border-zinc-800"
>
View Flow
</button>
<button className="p-3 text-zinc-800 hover:text-white transition-colors"><MoreHorizontal size={18} /></button>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* SDK Architect Modal */}
{isSdkModalOpen && (
<div className="fixed inset-0 z-[120] flex items-center justify-center p-6 backdrop-blur-md bg-black/80">
<div className="bg-zinc-950 border border-zinc-900 w-full max-w-5xl rounded-[3rem] p-10 shadow-2xl flex flex-col h-[85vh] animate-in zoom-in-95">
<div className="flex justify-between items-start mb-8">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-blue-600/10 text-blue-500 rounded-2xl flex items-center justify-center">
<Code size={28} />
</div>
<div>
<h3 className="text-2xl font-black text-white italic tracking-tighter uppercase">SDK <span className="text-blue-500 not-italic">Architect</span></h3>
<p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest flex items-center gap-2">
<Globe size={12} className="text-emerald-500" />
Targeting: {BRIDGE_URL}
</p>
</div>
</div>
<button onClick={() => setIsSdkModalOpen(false)} className="p-2 text-zinc-600 hover:text-white transition-colors"><X size={24} /></button>
</div>
<div className="flex-1 flex gap-8 overflow-hidden">
<div className="flex-1 flex flex-col bg-black border border-zinc-900 rounded-[2rem] overflow-hidden">
<div className="flex-1 overflow-y-auto p-8 space-y-8 custom-scrollbar">
{sdkChat.map((m, i) => (
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] p-6 rounded-[1.8rem] text-sm leading-relaxed ${m.role === 'user' ? 'bg-blue-600 text-white rounded-tr-none shadow-xl shadow-blue-900/20' : 'bg-zinc-900 text-zinc-300 rounded-tl-none border border-zinc-800'}`}>
{m.text}
</div>
</div>
))}
{isAiThinking && <ThinkingState />}
</div>
<div className="p-6 bg-zinc-900/50 border-t border-zinc-800 flex gap-4">
<input
value={sdkInput}
onChange={e => setSdkInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSdkChat()}
placeholder="e.g. Write a Node.js client to trigger a payment of $500..."
className="flex-1 bg-black border border-zinc-800 rounded-2xl px-6 py-4 text-white text-xs outline-none focus:border-blue-500 transition-all font-bold"
/>
<button onClick={handleSdkChat} disabled={isAiThinking} className="p-4 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl transition-all shadow-lg shadow-blue-900/40">
<ArrowRight size={20} />
</button>
</div>
</div>
<div className="w-1/2 bg-zinc-900 border border-zinc-800 rounded-[2rem] flex flex-col overflow-hidden">
<div className="bg-zinc-800/80 px-8 py-5 border-b border-zinc-700 flex justify-between items-center">
<div className="flex items-center gap-3">
<Terminal size={14} className="text-blue-500" />
<span className="text-[10px] font-black uppercase text-zinc-400">Implementation Script</span>
</div>
{finalCode && <button onClick={() => copyToClipboard(finalCode)} className="p-2 hover:bg-zinc-700 rounded-lg text-zinc-500 hover:text-white transition-all"><Copy size={16} /></button>}
</div>
<div className="flex-1 p-8 font-mono text-[11px] text-zinc-400 overflow-y-auto custom-scrollbar">
{finalCode ? <pre className="whitespace-pre-wrap">{finalCode}</pre> : (
<div className="h-full flex flex-col items-center justify-center text-center opacity-30">
<Cpu size={48} className="mb-4" />
<p className="text-[10px] font-black uppercase tracking-widest">Architect Idle</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Init Modal */}
{isInitModalOpen && (
<div className="fixed inset-0 z-[120] flex items-center justify-center p-6 backdrop-blur-md bg-black/80">
<div className="bg-zinc-950 border border-zinc-900 w-full max-w-lg rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95">
<div className="flex justify-between items-start mb-8">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-blue-600/10 text-blue-500 rounded-2xl flex items-center justify-center">
<Zap size={28} />
</div>
<h3 className="text-2xl font-black text-white italic tracking-tighter uppercase">Initialize <span className="text-blue-500 not-italic">Flow</span></h3>
</div>
<button onClick={() => setIsInitModalOpen(false)} className="p-2 text-zinc-500 hover:text-white transition-colors"><X size={24} /></button>
</div>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<button onClick={() => setInitType('account')} className={`p-6 rounded-2xl border transition-all ${initType === 'account' ? 'bg-blue-600/10 border-blue-600' : 'bg-zinc-900 border-zinc-800 opacity-50'}`}>
<UserPlus className="mb-4" size={20} />
<p className="text-white font-black text-[10px] uppercase">Account Col.</p>
</button>
<button onClick={() => setInitType('payment')} className={`p-6 rounded-2xl border transition-all ${initType === 'payment' ? 'bg-blue-600/10 border-blue-600' : 'bg-zinc-900 border-zinc-800 opacity-50'}`}>
<ArrowRightLeft className="mb-4" size={20} />
<p className="text-white font-black text-[10px] uppercase">Payment Onbd.</p>
</button>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Target CP</label>
<input value={initForm.counterpartyId} onChange={e => setInitForm({...initForm, counterpartyId: e.target.value})} className="w-full bg-black border border-zinc-800 rounded-xl p-4 text-white text-xs outline-none" />
</div>
{initType === 'payment' && (
<div className="space-y-2">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Limit (USD)</label>
<input type="number" value={initForm.amount} onChange={e => setInitForm({...initForm, amount: e.target.value})} className="w-full bg-black border border-zinc-800 rounded-xl p-4 text-white text-xs outline-none" />
</div>
)}
</div>
<button onClick={handleInitializeFlow} disabled={initLoading} className="w-full py-5 bg-blue-600 hover:bg-blue-500 text-white rounded-[2rem] font-black text-xs uppercase tracking-widest transition-all shadow-xl flex items-center justify-center gap-4">
{initLoading ? <Loader2 size={20} className="animate-spin" /> : <Plus size={20} />}
Establish Flow
</button>
</div>
</div>
</div>
)}
{/* Detail Modal */}
{selectedFlow && (
<div className="fixed inset-0 z-[130] flex items-center justify-center p-6 backdrop-blur-md bg-black/80">
<div className="bg-zinc-950 border border-zinc-900 w-full max-w-2xl rounded-[3.5rem] p-12 shadow-2xl animate-in zoom-in-95">
<div className="flex justify-between items-start mb-10 pb-8 border-b border-zinc-900">
<div className="flex items-center gap-6">
<div className="w-16 h-16 bg-blue-600/10 text-blue-500 rounded-3xl flex items-center justify-center border border-blue-500/20">
{selectedFlow.object === 'account_collection_flow' ? <UserPlus size={32} /> : <ArrowRightLeft size={32} />}
</div>
<div>
<h3 className="text-3xl font-black text-white italic tracking-tighter uppercase">{selectedFlow.id}</h3>
<div className="flex gap-3 mt-2">
<StatusBadge status={selectedFlow.status} />
<span className="text-[9px] font-black uppercase text-zinc-600 px-3 py-1 bg-zinc-900 rounded-full tracking-widest">LIVE_NODE</span>
</div>
</div>
</div>
<button onClick={() => setSelectedFlow(null)} className="p-3 text-zinc-600 hover:text-white transition-colors"><X size={24} /></button>
</div>
<div className="space-y-8">
<div className="grid grid-cols-2 gap-10">
<div>
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-widest mb-2">Registry Entity</p>
<p className="text-white font-bold">{selectedFlow.counterparty_id}</p>
</div>
<div>
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-widest mb-2">Sync Integrity</p>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse shadow-[0_0_8px_#10b981]"></div>
<span className="text-emerald-500 font-black text-[10px] uppercase">Node Synchronized</span>
</div>
</div>
</div>
<div className="p-8 bg-black border border-zinc-900 rounded-[2.5rem] space-y-4">
<div className="flex justify-between items-center">
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-widest">Node Handshake Token</p>
<button onClick={() => copyToClipboard(selectedFlow.client_token)} className="text-blue-500 hover:text-blue-400 transition-colors"><Copy size={16} /></button>
</div>
<div className="bg-zinc-950 p-5 rounded-2xl border border-zinc-900 font-mono text-[11px] text-blue-400/80 break-all select-all">
{selectedFlow.client_token}
</div>
</div>
<div className="flex gap-4">
<button className="flex-1 py-5 bg-zinc-900 hover:bg-zinc-800 text-white rounded-[2rem] font-black text-xs uppercase tracking-widest transition-all border border-zinc-800 flex items-center justify-center gap-3">
<Eye size={18} /> Inspect Signals
</button>
<button className="flex-1 py-5 bg-white hover:bg-zinc-100 text-black rounded-[2rem] font-black text-xs uppercase tracking-widest transition-all shadow-2xl">
Refresh Ledger
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
const ThinkingState = () => (
<div className="flex items-center gap-4 text-zinc-500">
<Loader2 className="animate-spin" size={18} />
<span className="text-[10px] font-black uppercase tracking-[0.3em] animate-pulse">Calculating Neural Implementation...</span>
</div>
);
const StatusBadge = ({ status }: { status: string }) => {
const styles: Record<string, string> = {
pending: 'bg-amber-500/10 text-amber-500 border border-amber-500/20',
completed: 'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20',
cancelled: 'bg-rose-500/10 text-rose-500 border border-rose-500/20',
};
return (
<div className={`inline-flex items-center px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest ${styles[status] || 'bg-zinc-800 text-zinc-500'}`}>
{status}
</div>
);
};
export default Flows;