Files
CurrenciCombo/src/components/RightPanel.tsx
Devin AI 52676016fb feat: Solace Bank Group PLC Treasury Management Portal
- Web3 authentication with MetaMask, WalletConnect, Coinbase wallet options
- Demo mode for testing without wallet
- Overview dashboard with KPI cards, asset allocation, positions, accounts, alerts
- Transaction Builder module (full IDE-style drag-and-drop canvas with 28 gap fixes)
- Accounts module with multi-account/subaccount hierarchical structures
- Treasury Management module with positions table and 14-day cash forecast
- Financial Reporting module with IPSAS, US GAAP, IFRS compliance
- Compliance & Risk module with KYC/AML/Sanctions monitoring
- Settlement & Clearing module with DVP/FOP/PVP operations
- Settings with role-based permissions and enterprise controls
- Dark theme professional UI with Solace Bank branding
- HashRouter for static hosting compatibility

Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
2026-04-18 17:20:13 +00:00

371 lines
18 KiB
TypeScript

import { useState, useRef, useEffect } from 'react';
import {
Send, Sparkles, Wrench, ShieldCheck, Route, FileText,
Landmark, AlertTriangle, BookOpen, ChevronDown, Plus,
Zap, RefreshCw, FileOutput, MessageSquare,
History, Target
} from 'lucide-react';
import type { Agent, ChatMessage, ConversationScope, ComponentItem } from '../types';
import { sampleMessages, sampleThreads } from '../data/sampleData';
import { componentItems } from '../data/components';
import type { Node, Edge } from '@xyflow/react';
const agents: { id: Agent; icon: typeof Sparkles; color: string }[] = [
{ id: 'Builder', icon: Sparkles, color: '#3b82f6' },
{ id: 'Compliance', icon: ShieldCheck, color: '#22c55e' },
{ id: 'Routing', icon: Route, color: '#f97316' },
{ id: 'ISO-20022', icon: FileText, color: '#a855f7' },
{ id: 'Settlement', icon: Landmark, color: '#eab308' },
{ id: 'Risk', icon: AlertTriangle, color: '#ef4444' },
{ id: 'Documentation', icon: BookOpen, color: '#6b7280' },
];
interface RightPanelProps {
width: number;
nodes: Node[];
edges: Edge[];
selectedNodes: Node[];
chatInputRef: React.RefObject<HTMLInputElement | null>;
onInsertBlock: (item: ComponentItem, position: { x: number; y: number }) => void;
onRunValidation: () => void;
onOptimizeRoute: () => void;
onRunCompliance: () => void;
onGenerateSettlement: () => void;
}
export default function RightPanel({
width, nodes, edges, selectedNodes, chatInputRef,
onInsertBlock, onRunValidation, onOptimizeRoute, onRunCompliance, onGenerateSettlement,
}: RightPanelProps) {
const [activeAgent, setActiveAgent] = useState<Agent>('Builder');
const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);
const [input, setInput] = useState('');
const [showAgentMenu, setShowAgentMenu] = useState(false);
const [showContext, setShowContext] = useState(false);
const [showThreads, setShowThreads] = useState(false);
const [scope, setScope] = useState<ConversationScope>('full-transaction');
const [showScopeMenu, setShowScopeMenu] = useState(false);
const messagesEnd = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const getCanvasContext = () => {
const nodeLabels = nodes.map(n => (n.data as Record<string, unknown>).label as string);
const categories = [...new Set(nodes.map(n => (n.data as Record<string, unknown>).category as string))];
const hasCompliance = categories.includes('compliance');
const hasRouting = categories.includes('routing');
const disconnected = nodes.filter(n => !edges.some(e => e.source === n.id || e.target === n.id));
const selectedLabels = selectedNodes.map(n => (n.data as Record<string, unknown>).label as string);
return { nodeLabels, categories, hasCompliance, hasRouting, disconnected, selectedLabels, nodeCount: nodes.length, edgeCount: edges.length };
};
const sendMessage = () => {
if (!input.trim()) return;
const userMsg: ChatMessage = {
id: Date.now().toString(),
agent: 'User',
content: input,
timestamp: new Date(),
type: 'user',
};
setMessages(prev => [...prev, userMsg]);
const capturedInput = input;
setInput('');
setTimeout(() => {
const ctx = getCanvasContext();
const buildResponse = (): string => {
const lowerInput = capturedInput.toLowerCase();
switch (activeAgent) {
case 'Builder': {
if (selectedNodes.length > 0) {
const sel = (selectedNodes[0].data as Record<string, unknown>).label as string;
if (lowerInput.includes('explain')) return `The "${sel}" block ${getBlockExplanation(sel)}. It currently has ${edges.filter(e => e.source === selectedNodes[0].id || e.target === selectedNodes[0].id).length} connection(s).`;
if (lowerInput.includes('next') || lowerInput.includes('suggest')) return `After "${sel}", I recommend adding a ${suggestNextBlock(sel)}. This would complete the ${(selectedNodes[0].data as Record<string, unknown>).category} flow.`;
}
if (lowerInput.includes('build') || lowerInput.includes('create') || lowerInput.includes('set up') || lowerInput.includes('payment')) {
if (ctx.nodeCount === 0) return `To build a transaction flow, start by dragging a "Fiat Account" or "Stablecoin Wallet" from the left panel as your source. Then add a "${capturedInput.includes('swap') ? 'Swap' : 'Transfer'}" action and connect them.`;
return `Your graph has ${ctx.nodeCount} nodes. Try dragging a "${capturedInput.includes('swap') ? 'Swap' : 'Transfer'}" block onto the canvas and connecting it to your source.`;
}
if (ctx.disconnected.length > 0) return `I notice ${ctx.disconnected.length} disconnected node(s) in your graph: ${ctx.disconnected.map(n => (n.data as Record<string, unknown>).label).join(', ')}. Connect them to complete the flow.`;
return `I can help you build that flow. Try dragging a "${capturedInput.includes('swap') ? 'Swap' : 'Transfer'}" block onto the canvas and connecting it to your source. Your graph currently has ${ctx.nodeCount} nodes and ${ctx.edgeCount} connections.`;
}
case 'Compliance': {
if (lowerInput.includes('check') || lowerInput.includes('compliance') || lowerInput.includes('review')) {
if (!ctx.hasCompliance && ctx.nodeCount > 0) return `WARNING: Your transaction graph has ${ctx.nodeCount} nodes but no compliance checks. I recommend adding KYC and AML nodes before the settlement step. This is required for cross-border transactions.`;
if (ctx.hasCompliance) return `Running compliance check on the current graph. No policy violations detected for the selected jurisdiction. ${ctx.nodeCount} nodes verified against 47 compliance rules.`;
return `Running compliance check on the current graph. No policy violations detected for the selected jurisdiction.`;
}
if (lowerInput.includes('violation') || lowerInput.includes('failure')) return `Scanning graph for policy violations... ${ctx.hasCompliance ? 'All compliance nodes are properly configured. No violations found.' : 'No compliance nodes found in graph. Consider adding KYC/AML checks.'}`;
return `Running compliance check on the current graph. No policy violations detected for the selected jurisdiction.`;
}
case 'Routing': {
if (ctx.hasRouting) return `Analyzing ${ctx.nodeCount} nodes with routing configuration. Found optimal path via ${ctx.nodeLabels.find(l => l.includes('Route') || l.includes('Router')) || 'Banking Rail'}. Estimated fee: 0.02%, latency: 230ms.`;
return `Analyzing optimal routes... Found 3 execution paths. The best route via Banking Rail offers lowest fees at 0.02%. Your graph has ${ctx.nodeCount} nodes across ${ctx.edgeCount} connections.`;
}
case 'ISO-20022': {
if (ctx.nodeCount > 0) return `Based on your graph with ${ctx.nodeCount} nodes, I can generate a pain.001 message. The required fields from your current configuration: debtor (${ctx.nodeLabels[0] || 'source'}), creditor (${ctx.nodeLabels[ctx.nodeLabels.length - 1] || 'destination'}), amount, and currency.`;
return `I can generate a pain.001 message for this transfer. The required fields based on your current graph are: debtor, creditor, amount, and currency.`;
}
case 'Settlement': {
return `Current settlement window for this transaction type is T+1. ${ctx.nodeCount > 0 ? `Your graph has ${ctx.nodeCount} nodes ready for settlement processing.` : 'I recommend adding a settlement instruction block to specify your preferred CSD.'}`;
}
case 'Risk': {
if (ctx.nodeCount > 0) {
const riskLevel = ctx.hasCompliance ? 'LOW' : 'MEDIUM';
return `Risk assessment: ${riskLevel}. ${ctx.nodeCount} nodes evaluated. ${ctx.hasCompliance ? 'Compliance checks present.' : 'No compliance nodes — risk elevated.'} ${ctx.disconnected.length > 0 ? `${ctx.disconnected.length} disconnected node(s) detected.` : 'All nodes connected.'}`;
}
return `Risk assessment: LOW. Transaction amount is within normal parameters. No counterparty risk flags detected.`;
}
case 'Documentation': {
if (ctx.nodeCount > 0) return `Generating deal memo for "${ctx.nodeLabels[0]}" flow with ${ctx.nodeCount} nodes. Categories: ${ctx.categories.join(', ')}. ${ctx.edgeCount} connections mapped. ${ctx.hasCompliance ? 'Compliance: verified.' : 'Compliance: not yet added.'}`;
return `I'll generate a deal memo for this transaction. It will include the execution path, compliance checks, and settlement instructions.`;
}
default:
return 'How can I assist you?';
}
};
const reply: ChatMessage = {
id: (Date.now() + 1).toString(),
agent: activeAgent,
content: buildResponse(),
timestamp: new Date(),
type: 'agent',
};
setMessages(prev => [...prev, reply]);
}, 800);
};
const currentAgentDef = agents.find(a => a.id === activeAgent)!;
const CurrentIcon = currentAgentDef.icon;
const scopeLabels: Record<ConversationScope, string> = {
'current-node': 'Current Node',
'current-flow': 'Current Flow',
'full-transaction': 'Full Transaction',
'terminal': 'Terminal',
'compliance': 'Compliance Only',
};
// Context from canvas
const ctx = getCanvasContext();
const handleInsertBlock = () => {
const suggestions = ['transfer', 'kyc', 'banking-rail'];
const item = componentItems.find(c => c.id === suggestions[Math.floor(Math.random() * suggestions.length)]);
if (item) onInsertBlock(item, { x: 250 + Math.random() * 200, y: 150 + Math.random() * 200 });
};
return (
<div className="right-panel" style={{ width }}>
<div className="panel-header">
<div className="chat-header-agent" onClick={() => setShowAgentMenu(!showAgentMenu)}>
<CurrentIcon size={14} color={currentAgentDef.color} />
<span>{activeAgent} Agent</span>
<ChevronDown size={12} />
{showAgentMenu && (
<div className="agent-dropdown">
{agents.map(a => {
const Icon = a.icon;
return (
<div
key={a.id}
className={`agent-option ${a.id === activeAgent ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setActiveAgent(a.id); setShowAgentMenu(false); }}
>
<Icon size={14} color={a.color} />
<span>{a.id}</span>
</div>
);
})}
</div>
)}
</div>
<div className="chat-header-actions">
<div className="scope-selector" onClick={() => setShowScopeMenu(!showScopeMenu)}>
<Target size={11} />
<span>{scopeLabels[scope]}</span>
<ChevronDown size={10} />
{showScopeMenu && (
<div className="scope-dropdown">
{(Object.keys(scopeLabels) as ConversationScope[]).map(s => (
<div
key={s}
className={`scope-option ${s === scope ? 'active' : ''}`}
onClick={e => { e.stopPropagation(); setScope(s); setShowScopeMenu(false); }}
>
{scopeLabels[s]}
</div>
))}
</div>
)}
</div>
<button
className={`icon-btn-xs ${showThreads ? 'active' : ''}`}
onClick={() => setShowThreads(!showThreads)}
title="Thread History"
>
<History size={12} />
</button>
<button
className={`context-toggle ${showContext ? 'active' : ''}`}
onClick={() => setShowContext(!showContext)}
title="Toggle context panel"
>
Context
</button>
</div>
</div>
<div className="agent-tabs">
{agents.map(a => {
const Icon = a.icon;
return (
<button
key={a.id}
className={`agent-tab ${a.id === activeAgent ? 'active' : ''}`}
onClick={() => setActiveAgent(a.id)}
title={a.id}
style={a.id === activeAgent ? { borderBottomColor: a.color } : {}}
>
<Icon size={13} color={a.id === activeAgent ? a.color : '#666'} />
</button>
);
})}
</div>
{showThreads && (
<div className="thread-history">
<div className="thread-history-header">Thread History</div>
{sampleThreads.map(t => (
<div key={t.id} className="thread-item" onClick={() => setShowThreads(false)}>
<MessageSquare size={12} color={agents.find(a => a.id === t.agent)?.color} />
<div className="thread-item-content">
<span className="thread-item-title">{t.title}</span>
<span className="thread-item-meta">{t.agent} · {t.messageCount} messages</span>
</div>
</div>
))}
</div>
)}
{showContext && (
<div className="context-panel">
<div className="context-section">
<span className="context-label">Selected</span>
<span className="context-value">{ctx.selectedLabels.length > 0 ? ctx.selectedLabels.join(', ') : 'None'}</span>
</div>
<div className="context-section">
<span className="context-label">Nodes</span>
<span className="context-value">{ctx.nodeCount}</span>
</div>
<div className="context-section">
<span className="context-label">Connections</span>
<span className="context-value">{ctx.edgeCount}</span>
</div>
<div className="context-section">
<span className="context-label">Jurisdiction</span>
<span className="context-value">Multi</span>
</div>
<div className="context-section">
<span className="context-label">Counterparties</span>
<span className="context-value">{ctx.nodeCount > 0 ? Math.max(1, Math.floor(ctx.nodeCount / 3)) : 0}</span>
</div>
<div className="context-section">
<span className="context-label">Compliance</span>
<span className={`context-value ${ctx.hasCompliance ? 'pass' : (ctx.nodeCount > 0 ? 'warn' : '')}`}>
{ctx.hasCompliance ? 'Pass' : (ctx.nodeCount > 0 ? 'Missing' : 'N/A')}
</span>
</div>
<div className="context-section">
<span className="context-label">Categories</span>
<span className="context-value">{ctx.categories.length > 0 ? ctx.categories.join(', ') : '—'}</span>
</div>
<div className="context-section">
<span className="context-label">Est. Fees</span>
<span className="context-value">{ctx.nodeCount > 0 ? '$0.02%' : '—'}</span>
</div>
</div>
)}
<div className="chat-messages">
{messages.map(msg => (
<div key={msg.id} className={`chat-message ${msg.type}`}>
<div className="message-header">
<span className="message-agent">{msg.agent}</span>
<span className="message-time">
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="message-content">{msg.content}</div>
</div>
))}
<div ref={messagesEnd} />
</div>
<div className="action-tray">
<button className="action-tray-btn" title="Insert recommended block" onClick={handleInsertBlock}>
<Plus size={12} /> Insert Block
</button>
<button className="action-tray-btn" title="Repair graph" onClick={onRunValidation}>
<Wrench size={12} /> Repair
</button>
<button className="action-tray-btn" title="Optimize route" onClick={onOptimizeRoute}>
<Zap size={12} /> Optimize
</button>
<button className="action-tray-btn" title="Run compliance" onClick={onRunCompliance}>
<ShieldCheck size={12} /> Comply
</button>
<button className="action-tray-btn" title="Generate settlement message" onClick={onGenerateSettlement}>
<FileOutput size={12} /> Settle
</button>
<button className="action-tray-btn" title="Refresh context" onClick={() => setShowContext(true)}>
<RefreshCw size={12} />
</button>
</div>
<div className="chat-input-area">
<input
ref={chatInputRef}
type="text"
placeholder={`Ask ${activeAgent} Agent...`}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && sendMessage()}
/>
<button className="send-btn" onClick={sendMessage} disabled={!input.trim()}>
<Send size={14} />
</button>
</div>
</div>
);
}
function getBlockExplanation(label: string): string {
const explanations: Record<string, string> = {
'Fiat Account': 'represents a traditional fiat currency account, typically used as a source or destination for fund transfers',
'Transfer': 'moves value from one account to another along a defined path',
'KYC': 'performs Know Your Customer verification before allowing the transaction to proceed',
'AML': 'runs Anti-Money Laundering screening against watchlists',
'Swap': 'exchanges one asset type for another at the current market rate',
'Banking Rail': 'routes the transaction through traditional banking infrastructure',
};
return explanations[label] || 'is a transaction primitive used in flow composition';
}
function suggestNextBlock(label: string): string {
const suggestions: Record<string, string> = {
'Fiat Account': 'Transfer or Convert block',
'Transfer': 'KYC compliance check',
'KYC': 'AML screening node',
'AML': 'Banking Rail or DEX Route',
'Swap': 'Settlement instruction',
'Banking Rail': 'Settlement instruction',
};
return suggestions[label] || 'compliance or routing node';
}