2586b81
73859c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>PSD3 & PSR Gap Analyser | PwC Data & AI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css"/>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-background-primary: #ffffff;
--color-background-secondary: #f7f7f5;
--color-background-tertiary: #efefec;
--color-background-info: #e6f1fb;
--color-background-success: #eaf3de;
--color-background-warning: #faeeda;
--color-background-danger: #fcebeb;
--color-text-primary: #1a1a18;
--color-text-secondary: #6b6b68;
--color-text-tertiary: #9b9b97;
--color-text-info: #185fa5;
--color-text-success: #3b6d11;
--color-text-warning: #854f0b;
--color-text-danger: #a32d2d;
--color-border-tertiary: rgba(0,0,0,0.10);
--color-border-secondary: rgba(0,0,0,0.18);
--color-border-primary: rgba(0,0,0,0.26);
--border-radius-md: 8px;
--border-radius-lg: 12px;
--border-radius-xl: 16px;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Courier New', monospace;
}
body { font-family: var(--font-sans); background: #f4f3f0; min-height: 100vh; color: var(--color-text-primary); }
#root { max-width: 980px; margin: 0 auto; padding: 24px 16px; }
input[type="text"], input[type="password"], textarea {
width: 100%; padding: 9px 12px; font-size: 14px; font-family: var(--font-sans);
border: 0.5px solid var(--color-border-secondary); border-radius: var(--border-radius-md);
background: var(--color-background-primary); color: var(--color-text-primary);
outline: none; transition: border-color .15s;
}
input[type="text"]:focus, input[type="password"]:focus, textarea:focus {
border-color: #D04A02; box-shadow: 0 0 0 3px rgba(208,74,2,0.12);
}
button { font-family: var(--font-sans); cursor: pointer; }
a { color: #D04A02; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; }
@keyframes spin { to { transform: rotate(360deg); } }
.spin { animation: spin 1s linear infinite; display: inline-block; }
@media (max-width: 640px) {
.gap-table-header, .gap-table-row { grid-template-columns: 20px 1fr 60px !important; }
.gap-col-psd2, .gap-col-psd3, .gap-col-psr { display: none; }
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useRef, useEffect, useCallback } = React;
const GAP_DATA = [
{ area:"Strong Customer Authentication (SCA)", icon:"ti-lock", psd2:"Mandatory SCA for online payments; exemptions for low-value, trusted beneficiaries, low-risk transactions", psd3:"Refines SCA exemptions; new exemptions for recurring payments; banks must not block TPP SCA flows", psr:"Harmonises SCA rules into directly applicable regulation; sets technical standards centrally", gap:"Review SCA exemption logic; update technical flows for TPP compatibility; align with EBA technical standards", severity:"medium" },
{ area:"Open Banking API Standards", icon:"ti-api", psd2:"Banks must provide APIs for TPPs but no mandatory standard; performance requirements weak; fallback interfaces allowed", psd3:"Mandatory dedicated interfaces; performance benchmarks enforceable; no fallback screen scraping; consent dashboards required", psr:"Standardised API specs mandated EU-wide; API performance KPIs set centrally; real-time monitoring obligations", gap:"Audit API quality against new KPIs; remove fallback interfaces; build consent management dashboards; implement API monitoring", severity:"high" },
{ area:"Third Party Provider (TPP) Access", icon:"ti-plug", psd2:"AISPs and PISPs can access payment accounts with customer consent; banks cannot block legitimate TPPs", psd3:"Expanded TPP rights; banks must provide equal access as internal systems; new dispute resolution for access refusals", psr:"Open finance extended to savings, investments, pensions, insurance; new Financial Information Service Provider (FISP) category", gap:"Extend data sharing beyond payment accounts; onboard FISP frameworks; review internal vs external data parity", severity:"high" },
{ area:"Fraud Liability & APP Fraud", icon:"ti-shield-exclamation", psd2:"Liability on PSP for unauthorised transactions; limited rules on authorised push payment (APP) fraud", psd3:"Explicit APP fraud liability framework; banks refund victims unless gross negligence proven; mandatory fraud data sharing", psr:"Mandatory fraud data sharing registers; harmonised APP refund timelines; AI-based transaction monitoring encouraged", gap:"Build APP fraud detection models; establish fraud data sharing protocols; update refund procedures; deploy AI monitoring", severity:"high" },
{ area:"Consumer Protection & Transparency", icon:"ti-user-check", psd2:"Pre-contractual info requirements; 13-month statement access; right to refund for direct debits", psd3:"Enhanced transparency for currency conversion; clearer IBAN/name mismatch warnings; improved accessibility rules", psr:"Centralised disclosure templates; machine-readable transaction data; standardised terminology across EU", gap:"Update disclosures; implement IBAN/name mismatch checks; make transaction data machine-readable", severity:"low" },
{ area:"Data Governance & Quality", icon:"ti-database", psd2:"No specific data quality standards; data sharing limited to payment account data", psd3:"Data accuracy obligations; requires data lineage and audit trails for shared financial data", psr:"Mandatory data quality standards; structured data schemas for open finance; governance frameworks must be documented", gap:"Implement data quality frameworks; build data lineage tooling; document governance policies; extend to open finance", severity:"high" },
{ area:"Licensing & Registration", icon:"ti-certificate", psd2:"Separate licensing for PIs and EMIs; passporting across EU; EBA register of authorised entities", psd3:"Streamlined licensing for combined PI/EMI activities; faster passporting; updated fitness and propriety requirements", psr:"New FISP registration category; harmonised registration process; central EU-wide register under EBA", gap:"Review licence scope against new combined categories; update FISP registration if applicable; audit fitness requirements", severity:"medium" },
{ area:"Reporting & Compliance", icon:"ti-report", psd2:"Incident reporting to NCAs; statistical reporting; limited standardisation", psd3:"Enhanced major incident reporting timelines; operational resilience aligned with DORA; more granular statistical data", psr:"Centralised reporting templates; automated regulatory reporting encouraged; real-time incident notification thresholds", gap:"Align incident reporting with DORA; build automated reporting pipelines; update NCA notification workflows", severity:"medium" },
{ area:"Non-Bank Access to Payment Systems", icon:"ti-building-bank", psd2:"Limited; fintechs mostly access via bank-sponsored model", psd3:"Non-bank PSPs can apply for direct access to payment systems (SEPA) without a bank sponsor; ECB and NCBs must provide access", psr:"Mandatory access rights for qualifying non-bank PSPs; central bank account access for e-money institutions", gap:"Evaluate direct access eligibility; build central bank settlement capability if applicable; update liquidity models", severity:"medium" },
{ area:"Dispute Resolution", icon:"ti-gavel", psd2:"ADR schemes required; 15-business-day response time for complaints", psd3:"Faster dispute timelines; standardised complaint categories; dedicated TPP-bank arbitration path", psr:"Harmonised EU dispute resolution body; machine-readable complaint data; structured escalation paths", gap:"Update complaint SLAs; implement structured complaint data capture; prepare for cross-border escalation", severity:"low" },
];
const SEV = {
high: { bg:"#FCEBEB", text:"#A32D2D", dot:"#E24B4A", label:"High Gap" },
medium: { bg:"#FAEEDA", text:"#854F0B", dot:"#EF9F27", label:"Medium Gap" },
low: { bg:"#EAF3DE", text:"#3B6D11", dot:"#639922", label:"Low Gap" },
};
const SYSTEM_PROMPT = `You are an expert EU payments regulatory advisor specialising in PSD2, PSD3, and the Payment Services Regulation (PSR). You work at PwC in the Data & AI practice and help clients understand compliance gaps between current PSD2 and incoming PSD3/PSR regulations.
Key facts:
- PSD2: current regulation. Most banks are PSD2-compliant.
- PSD3: Directive (needs national transposition) expected 2026-2027.
- PSR: Regulation (directly applicable EU-wide) companion to PSD3.
- PSR extends open banking into open finance (investments, pensions, savings, insurance).
- Key changes: API standardisation, APP fraud liability, non-bank access to payment systems, FISP category, data quality standards.
- Data & AI angle: fraud detection models, open finance data pipelines, RegTech automation, data governance frameworks.
Be concise, precise, and always relate answers to practical gaps a PSD2-compliant firm faces. Highlight Data & AI implications. Use plain English. Format clearly with short paragraphs and bullet points for action lists.`;
const SEARCH_QUERIES = [
{ key:"psd3", label:"PSD3 Directive", query:"PSD3 Payment Services Directive 3 EUR-Lex 2025 2026 legislative status", icon:"ti-file-euro" },
{ key:"psr", label:"Payment Services Regulation (PSR)", query:"Payment Services Regulation PSR COM 2023 367 EUR-Lex legislative status", icon:"ti-file-euro" },
{ key:"psd2", label:"PSD2 (Current)", query:"PSD2 Directive 2015/2366 EUR-Lex current status", icon:"ti-building-bank" },
{ key:"fida", label:"FIDA — Financial Data Access", query:"FIDA Financial Data Access Regulation COM 2023 360 EUR-Lex open finance", icon:"ti-database" },
{ key:"dora", label:"DORA — Digital Operational Resilience", query:"DORA Digital Operational Resilience Regulation 2022/2554 payment services", icon:"ti-lock" },
{ key:"eba", label:"EBA Payment Services Guidelines", query:"EBA guidelines payment services PSD3 PSR 2025 2026 eba.europa.eu", icon:"ti-shield" },
];
const REF_SYSTEM = `You are a regulatory research assistant. Given web search results about EU payment services legislation, return ONLY a JSON object (no markdown, no extra text):
{"title":"Official title","reference":"Document reference e.g. COM(2023) 366","status":"Legislative status e.g. In force / Under negotiation / Proposal","summary":"2-3 sentence factual summary","eurlex_url":"Direct EUR-Lex URL or null","last_updated":"Most recent date mentioned or null","key_articles":"1-2 key provisions or null"}
Be factual and precise. Base answers only on search results provided.`;
function ApiKeyScreen({ onSave }) {
const [key, setKey] = useState("");
return (
<div style={{ minHeight:"100vh", display:"flex", alignItems:"center", justifyContent:"center" }}>
<div style={{ background:"#fff", borderRadius:16, border:"0.5px solid rgba(0,0,0,0.1)", padding:"2.5rem", maxWidth:460, width:"100%" }}>
<div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:"1.5rem" }}>
<div style={{ background:"#D04A02", color:"#fff", fontWeight:700, fontSize:13, padding:"3px 10px", borderRadius:4 }}>PwC</div>
<span style={{ fontSize:13, color:"#6b6b68" }}>Data & AI Practice</span>
</div>
<div style={{ fontSize:22, fontWeight:500, marginBottom:8 }}>PSD3 & PSR Gap Analyser</div>
<div style={{ fontSize:14, color:"#6b6b68", marginBottom:"1.5rem", lineHeight:1.6 }}>
Enter your Anthropic API key to activate the AI Assessor and live EUR-Lex search. Your key stays in memory only and is never stored.
</div>
<label style={{ fontSize:13, fontWeight:500, color:"#1a1a18", display:"block", marginBottom:6 }}>Anthropic API Key</label>
<input
type="password"
placeholder="sk-ant-..."
value={key}
onChange={e => setKey(e.target.value)}
onKeyDown={e => e.key === "Enter" && key.startsWith("sk-") && onSave(key)}
style={{ marginBottom:12 }}
/>
<button
onClick={() => onSave(key)}
disabled={!key.startsWith("sk-")}
style={{ width:"100%", padding:"10px", background: key.startsWith("sk-") ? "#D04A02" : "#ccc", color:"#fff", border:"none", borderRadius:8, fontSize:14, fontWeight:500, cursor: key.startsWith("sk-") ? "pointer" : "not-allowed" }}
>
Launch Tool
</button>
<div style={{ marginTop:12, fontSize:12, color:"#9b9b97", textAlign:"center" }}>
Get your key at <a href="https://console.anthropic.com" target="_blank" rel="noreferrer">console.anthropic.com</a>
</div>
</div>
</div>
);
}
function RefsTab({ apiKey }) {
const [refs, setRefs] = useState({});
const [loadingMap, setLoadingMap] = useState({});
const [fetched, setFetched] = useState(false);
async function fetchAll() {
setFetched(true);
for (const q of SEARCH_QUERIES) {
setLoadingMap(p => ({ ...p, [q.key]: true }));
try {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method:"POST",
headers:{ "Content-Type":"application/json", "x-api-key": apiKey, "anthropic-version":"2023-06-01", "anthropic-dangerous-direct-browser-access":"true" },
body: JSON.stringify({ model:"claude-sonnet-4-20250514", max_tokens:800, system:REF_SYSTEM, tools:[{ type:"web_search_20250305", name:"web_search" }], messages:[{ role:"user", content:`Search for current information about: ${q.query}. Return structured JSON only.` }] }),
});
const data = await res.json();
const text = (data.content||[]).filter(b=>b.type==="text").map(b=>b.text).join("");
const match = text.replace(/```json|```/g,"").trim().match(/\{[\s\S]*\}/);
if (match) setRefs(p => ({ ...p, [q.key]: JSON.parse(match[0]) }));
else setRefs(p => ({ ...p, [q.key]: { error: true } }));
} catch { setRefs(p => ({ ...p, [q.key]: { error: true } })); }
setLoadingMap(p => ({ ...p, [q.key]: false }));
}
}
const isAnyLoading = Object.values(loadingMap).some(Boolean);
const statusStyle = s => {
if (!s) return { bg:"#F1EFE8", text:"#5F5E5A" };
const sl = s.toLowerCase();
if (sl.includes("force")) return { bg:"#EAF3DE", text:"#3B6D11" };
if (sl.includes("negotiat") || sl.includes("process") || sl.includes("proposal")) return { bg:"#FAEEDA", text:"#854F0B" };
if (sl.includes("transpos") || sl.includes("await")) return { bg:"#E6F1FB", text:"#185FA5" };
return { bg:"#F1EFE8", text:"#5F5E5A" };
};
return (
<div>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:"1rem", flexWrap:"wrap", gap:8 }}>
<div>
<div style={{ fontSize:14, fontWeight:500 }}>Live EU Regulation References</div>
<div style={{ fontSize:12, color:"#6b6b68", marginTop:2 }}>Fetched in real time from EUR-Lex, EBA, and European Parliament</div>
</div>
<button onClick={fetchAll} disabled={isAnyLoading} style={{ display:"flex", alignItems:"center", gap:6, fontSize:13, padding:"7px 14px", background:"#D04A02", color:"#fff", border:"none", borderRadius:8, fontWeight:500, opacity: isAnyLoading ? 0.6 : 1 }}>
<i className={`ti ti-search${isAnyLoading?" spin":""}`} style={{ fontSize:14 }} />
{!fetched ? "Fetch live from EUR-Lex" : isAnyLoading ? "Fetching…" : "Refresh"}
</button>
</div>
{!fetched ? (
<div style={{ background:"#f7f7f5", border:"0.5px solid rgba(0,0,0,0.1)", borderRadius:12, padding:"2.5rem", textAlign:"center" }}>
<i className="ti ti-world" style={{ fontSize:36, color:"#D04A02", marginBottom:12, display:"block" }} />
<div style={{ fontSize:14, fontWeight:500, marginBottom:6 }}>Ready to fetch live EU regulation data</div>
<div style={{ fontSize:13, color:"#6b6b68", marginBottom:16, maxWidth:400, margin:"0 auto 16px" }}>Searches EUR-Lex, EBA, and the European Parliament in real time for the latest legislative status, document references, and summaries.</div>
<button onClick={fetchAll} style={{ fontSize:14, padding:"8px 20px", background:"#D04A02", color:"#fff", border:"none", borderRadius:8, fontWeight:500 }}>
<i className="ti ti-search" style={{ fontSize:14, marginRight:6 }} />Fetch live from EUR-Lex
</button>
</div>
) : (
<div style={{ display:"grid", gap:"0.75rem" }}>
{SEARCH_QUERIES.map(q => {
const ref = refs[q.key];
const loading = loadingMap[q.key];
const ss = statusStyle(ref?.status);
return (
<div key={q.key} style={{ background:"#fff", border:"0.5px solid rgba(0,0,0,0.1)", borderRadius:12, padding:"1rem 1.25rem" }}>
{loading ? (
<div style={{ display:"flex", alignItems:"center", gap:10 }}>
<i className={`ti ${q.icon}`} style={{ fontSize:20, color:"#D04A02" }} />
<div>
<div style={{ fontSize:13, fontWeight:500 }}>{q.label}</div>
<div style={{ fontSize:12, color:"#6b6b68", marginTop:4, display:"flex", alignItems:"center", gap:6 }}>
<i className="ti ti-loader-2 spin" style={{ fontSize:13 }} /> Searching EUR-Lex…
</div>
</div>
</div>
) : ref?.error ? (
<div style={{ display:"flex", alignItems:"center", gap:10 }}>
<i className={`ti ${q.icon}`} style={{ fontSize:20, color:"#D04A02" }} />
<div>
<div style={{ fontSize:13, fontWeight:500 }}>{q.label}</div>
<div style={{ fontSize:12, color:"#6b6b68", marginTop:4 }}>Could not retrieve. Try refreshing.</div>
</div>
</div>
) : ref ? (
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"flex-start", gap:12, flexWrap:"wrap" }}>
<div style={{ display:"flex", gap:10, alignItems:"flex-start", flex:1 }}>
<i className={`ti ${q.icon}`} style={{ fontSize:20, color:"#D04A02", marginTop:2, flexShrink:0 }} />
<div style={{ flex:1 }}>
<div style={{ fontSize:14, fontWeight:500, marginBottom:2 }}>{ref.title||q.label}</div>
{ref.reference && <div style={{ fontSize:12, color:"#6b6b68", marginBottom:6 }}>{ref.reference}{ref.last_updated?` · Updated ${ref.last_updated}`:""}</div>}
{ref.summary && <div style={{ fontSize:13, color:"#6b6b68", lineHeight:1.6, marginBottom: ref.key_articles?8:0 }}>{ref.summary}</div>}
{ref.key_articles && <div style={{ fontSize:12, background:"#f7f7f5", borderRadius:4, padding:"6px 10px", marginTop:6, color:"#6b6b68", borderLeft:"2px solid #D04A02" }}><span style={{ fontWeight:500, color:"#1a1a18" }}>Key provisions: </span>{ref.key_articles}</div>}
</div>
</div>
<div style={{ display:"flex", flexDirection:"column", alignItems:"flex-end", gap:8, flexShrink:0 }}>
{ref.status && <span style={{ fontSize:11, padding:"3px 8px", borderRadius:3, background:ss.bg, color:ss.text, fontWeight:500, whiteSpace:"nowrap" }}>{ref.status}</span>}
{ref.eurlex_url && <a href={ref.eurlex_url} target="_blank" rel="noreferrer" style={{ fontSize:12, color:"#D04A02", textDecoration:"none", display:"flex", alignItems:"center", gap:4 }}>View on EUR-Lex <i className="ti ti-external-link" style={{ fontSize:11 }} /></a>}
</div>
</div>
) : (
<div style={{ display:"flex", alignItems:"center", gap:10 }}>
<i className={`ti ${q.icon}`} style={{ fontSize:20, color:"#6b6b68" }} />
<div style={{ fontSize:13, color:"#6b6b68" }}>{q.label}</div>
</div>
)}
</div>
);
})}
<div style={{ background:"#f7f7f5", borderRadius:8, padding:"10px 14px", fontSize:12, color:"#6b6b68", display:"flex", alignItems:"center", gap:6 }}>
<i className="ti ti-world" style={{ fontSize:13 }} /> Data sourced live from EUR-Lex, EBA, and European Parliament. Always verify against primary source documents.
</div>
</div>
)}
</div>
);
}
function App() {
const [apiKey, setApiKey] = useState("");
const [tab, setTab] = useState("chat");
const [messages, setMessages] = useState([{ role:"assistant", content:"Hello! I'm your PSD3 & PSR compliance advisor. Ask me anything about the regulatory changes and how they affect your current PSD2 compliance — I'll help you identify gaps and understand what your Data & AI team needs to build." }]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState("all");
const [expandedRow, setExpandedRow] = useState(null);
const messagesEndRef = useRef(null);
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior:"smooth" }); }, [messages]);
if (!apiKey) return <ApiKeyScreen onSave={setApiKey} />;
const sendMessage = async () => {
if (!input.trim() || loading) return;
const userMsg = { role:"user", content:input };
const newMessages = [...messages, userMsg];
setMessages(newMessages);
setInput("");
setLoading(true);
try {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method:"POST",
headers:{ "Content-Type":"application/json", "x-api-key":apiKey, "anthropic-version":"2023-06-01", "anthropic-dangerous-direct-browser-access":"true" },
body: JSON.stringify({ model:"claude-sonnet-4-20250514", max_tokens:1000, system:SYSTEM_PROMPT, messages:newMessages.map(m=>({ role:m.role, content:m.content })) }),
});
const data = await res.json();
const reply = (data.content||[]).map(b=>b.text||"").join("") || "Sorry, I couldn't generate a response.";
setMessages([...newMessages, { role:"assistant", content:reply }]);
} catch { setMessages([...newMessages, { role:"assistant", content:"Connection error. Please check your API key and try again." }]); }
setLoading(false);
};
const filtered = filter === "all" ? GAP_DATA : GAP_DATA.filter(r => r.severity === filter);
const counts = { high: GAP_DATA.filter(r=>r.severity==="high").length, medium: GAP_DATA.filter(r=>r.severity==="medium").length, low: GAP_DATA.filter(r=>r.severity==="low").length };
const QUICK_Qs = ["What are the biggest Data & AI gaps under PSD3?", "How does APP fraud liability change for banks?", "What is open finance and how does PSR enable it?", "What new data quality standards does PSR introduce?", "How should we build a PSD3 fraud detection model?"];
return (
<div style={{ background:"#fff", borderRadius:16, border:"0.5px solid rgba(0,0,0,0.08)", overflow:"hidden" }}>
{/* Header */}
<div style={{ borderBottom:"3px solid #D04A02", padding:"1.25rem 1.5rem" }}>
<div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:6 }}>
<div style={{ background:"#D04A02", color:"#fff", fontWeight:700, fontSize:13, padding:"3px 10px", borderRadius:4 }}>PwC</div>
<span style={{ fontSize:13, color:"#6b6b68" }}>Data & AI Practice</span>
<span style={{ marginLeft:"auto", fontSize:12, color:"#9b9b97", cursor:"pointer" }} onClick={() => setApiKey("")}>⬡ Change key</span>
</div>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"flex-end", flexWrap:"wrap", gap:8 }}>
<div>
<div style={{ fontSize:20, fontWeight:500, lineHeight:1.2 }}>PSD2 → PSD3 & PSR Gap Analyser</div>
<div style={{ fontSize:13, color:"#6b6b68", marginTop:4 }}>AI-powered compliance assessment · May 2026</div>
</div>
<div style={{ display:"flex", gap:8 }}>
{[["high",counts.high],["medium",counts.medium],["low",counts.low]].map(([s,c]) => (
<div key={s} style={{ background:SEV[s].bg, color:SEV[s].text, fontSize:12, padding:"4px 10px", borderRadius:4, fontWeight:500 }}>{c} {s}</div>
))}
</div>
</div>
</div>
{/* Tabs */}
<div style={{ display:"flex", gap:4, borderBottom:"0.5px solid rgba(0,0,0,0.1)", padding:"0 1.5rem" }}>
{[["chat","ti-message-circle","AI Assessor"],["gap","ti-table","Gap Analysis"],["refs","ti-file-text","EU References"]].map(([id,icon,label]) => (
<button key={id} onClick={() => setTab(id)} style={{ background:"transparent", border:"none", borderBottom: tab===id?"2px solid #D04A02":"2px solid transparent", padding:"10px 14px", cursor:"pointer", fontSize:14, fontWeight: tab===id?500:400, color: tab===id?"#D04A02":"#6b6b68", display:"flex", alignItems:"center", gap:6, marginBottom:-1 }}>
<i className={`ti ${icon}`} style={{ fontSize:15 }} />{label}
</button>
))}
</div>
<div style={{ padding:"1.5rem" }}>
{/* Chat Tab */}
{tab === "chat" && (
<div>
<div style={{ background:"#f7f7f5", borderRadius:12, border:"0.5px solid rgba(0,0,0,0.08)", height:380, overflowY:"auto", padding:"1rem", marginBottom:"0.75rem" }}>
{messages.map((m,i) => (
<div key={i} style={{ marginBottom:"1rem", display:"flex", gap:10, alignItems:"flex-start", flexDirection: m.role==="user"?"row-reverse":"row" }}>
<div style={{ width:30, height:30, borderRadius:"50%", flexShrink:0, display:"flex", alignItems:"center", justifyContent:"center", fontSize:12, fontWeight:500, background: m.role==="user"?"#D04A02":"#fff", color: m.role==="user"?"#fff":"#6b6b68", border: m.role==="assistant"?"0.5px solid rgba(0,0,0,0.1)":"none" }}>
{m.role==="user" ? "Y" : <i className="ti ti-robot" style={{ fontSize:14 }} />}
</div>
<div style={{ maxWidth:"80%", background: m.role==="user"?"#D04A02":"#fff", color: m.role==="user"?"#fff":"#1a1a18", padding:"10px 14px", borderRadius:8, border: m.role==="assistant"?"0.5px solid rgba(0,0,0,0.08)":"none", fontSize:14, lineHeight:1.6, whiteSpace:"pre-wrap" }}>
{m.content}
</div>
</div>
))}
{loading && (
<div style={{ display:"flex", gap:10, alignItems:"center" }}>
<div style={{ width:30, height:30, borderRadius:"50%", background:"#fff", border:"0.5px solid rgba(0,0,0,0.1)", display:"flex", alignItems:"center", justifyContent:"center" }}>
<i className="ti ti-robot" style={{ fontSize:14 }} />
</div>
<div style={{ fontSize:14, color:"#6b6b68" }}>Analysing regulation…</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div style={{ display:"flex", flexWrap:"wrap", gap:6, marginBottom:"0.75rem" }}>
{QUICK_Qs.map((q,i) => (
<button key={i} onClick={() => setInput(q)} style={{ fontSize:12, padding:"5px 10px", border:"0.5px solid rgba(0,0,0,0.15)", borderRadius:4, background:"transparent", cursor:"pointer", color:"#6b6b68" }}>{q}</button>
))}
</div>
<div style={{ display:"flex", gap:8 }}>
<input type="text" value={input} onChange={e=>setInput(e.target.value)} onKeyDown={e=>e.key==="Enter"&&!e.shiftKey&&sendMessage()} placeholder="Ask about PSD3 / PSR compliance gaps…" />
<button onClick={sendMessage} disabled={loading||!input.trim()} style={{ padding:"0 16px", background:"#D04A02", color:"#fff", border:"none", borderRadius:8, fontSize:14, fontWeight:500, opacity: loading||!input.trim()?0.5:1 }}>
<i className="ti ti-send" />
</button>
</div>
</div>
)}
{/* Gap Table Tab */}
{tab === "gap" && (
<div>
<div style={{ display:"flex", gap:8, marginBottom:"1rem", flexWrap:"wrap" }}>
{[["all","All areas"],["high","High gap"],["medium","Medium gap"],["low","Low gap"]].map(([v,l]) => (
<button key={v} onClick={() => setFilter(v)} style={{ fontSize:13, padding:"5px 12px", borderRadius:4, cursor:"pointer", fontWeight: filter===v?500:400, background: filter===v?"#D04A02":"transparent", color: filter===v?"#fff":"#6b6b68", border: filter===v?"none":"0.5px solid rgba(0,0,0,0.15)" }}>{l}</button>
))}
</div>
<div style={{ overflowX:"auto" }}>
<div style={{ minWidth:700 }}>
<div className="gap-table-header" style={{ display:"grid", gridTemplateColumns:"28px 1fr 1fr 1fr 1fr 80px", background:"#464646", color:"#fff", fontSize:12, fontWeight:500, padding:"8px 12px", borderRadius:"8px 8px 0 0" }}>
<div></div><div>Area</div>
<div className="gap-col-psd2">PSD2 (Current)</div>
<div className="gap-col-psd3">PSD3 (Incoming)</div>
<div className="gap-col-psr">PSR (Incoming)</div>
<div style={{ textAlign:"center" }}>Gap</div>
</div>
{filtered.map((row,i) => {
const sev = SEV[row.severity];
const expanded = expandedRow === i;
return (
<div key={i} style={{ border:"0.5px solid rgba(0,0,0,0.1)", borderTop:"none", background: i%2===0?"#fff":"#f7f7f5" }}>
<div className="gap-table-row" onClick={() => setExpandedRow(expanded?null:i)} style={{ display:"grid", gridTemplateColumns:"28px 1fr 1fr 1fr 1fr 80px", padding:"10px 12px", cursor:"pointer", alignItems:"flex-start" }}>
<div style={{ paddingTop:2 }}><i className={`ti ${row.icon}`} style={{ fontSize:15, color:"#6b6b68" }} /></div>
<div style={{ fontSize:13, fontWeight:500, paddingRight:8 }}>{row.area}</div>
<div className="gap-col-psd2" style={{ fontSize:12, color:"#6b6b68", paddingRight:8, lineHeight:1.5 }}>{expanded?row.psd2:row.psd2.slice(0,65)+"…"}</div>
<div className="gap-col-psd3" style={{ fontSize:12, color:"#6b6b68", paddingRight:8, lineHeight:1.5 }}>{expanded?row.psd3:row.psd3.slice(0,65)+"…"}</div>
<div className="gap-col-psr" style={{ fontSize:12, color:"#6b6b68", paddingRight:8, lineHeight:1.5 }}>{expanded?row.psr:row.psr.slice(0,65)+"…"}</div>
<div style={{ textAlign:"center" }}><span style={{ background:sev.bg, color:sev.text, fontSize:11, padding:"3px 7px", borderRadius:3, fontWeight:500, whiteSpace:"nowrap" }}>{sev.label}</span></div>
</div>
{expanded && (
<div style={{ margin:"0 12px 12px 40px", padding:"10px 14px", background:sev.bg, borderRadius:8, borderLeft:`3px solid ${sev.dot}` }}>
<div style={{ fontSize:12, fontWeight:500, color:sev.text, marginBottom:4 }}>
<i className="ti ti-arrow-right" style={{ fontSize:12, marginRight:4 }} />Recommended gap actions
</div>
<div style={{ fontSize:13, lineHeight:1.6 }}>{row.gap}</div>
<button style={{ marginTop:8, fontSize:12, padding:"4px 10px", background:"transparent", border:`0.5px solid ${sev.dot}`, color:sev.text, borderRadius:4, cursor:"pointer" }}
onClick={e => { e.stopPropagation(); setTab("chat"); setInput(`Tell me more about the gap in "${row.area}" and what the Data & AI team should build`); }}>
Ask AI about this area ↗
</button>
</div>
)}
</div>
);
})}
<div style={{ borderRadius:"0 0 8px 8px", border:"0.5px solid rgba(0,0,0,0.1)", borderTop:"none", padding:"8px 12px", background:"#f7f7f5" }}>
<span style={{ fontSize:12, color:"#6b6b68" }}>Click any row to expand full details and gap actions.</span>
</div>
</div>
</div>
</div>
)}
{/* EU References Tab */}
{tab === "refs" && <RefsTab apiKey={apiKey} />}
</div>
{/* Footer */}
<div style={{ borderTop:"0.5px solid rgba(0,0,0,0.08)", padding:"12px 1.5rem", display:"flex", justifyContent:"space-between", alignItems:"center", flexWrap:"wrap", gap:8 }}>
<span style={{ fontSize:12, color:"#9b9b97" }}>PwC Data & AI Practice · PSD3 & PSR Gap Analyser · May 2026</span>
<span style={{ fontSize:12, color:"#9b9b97" }}>AI responses for guidance only — verify against primary sources</span>
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(React.createElement(App));
</script>
</body>
</html>
|