File size: 33,948 Bytes
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>