CoolFace
Modelpublic

zombee11/scorecardapi

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes
index.html893 linesDownload Raw Back to root
1<!doctype html>2<html lang="en">3  <head>4    <meta charset="utf-8" />5    <meta name="viewport" content="width=device-width, initial-scale=1" />6    <title>Two Wheeler Scorecard Console</title>7    <style>8      :root {9        --bg: #f4f7fb;10        --panel: #ffffff;11        --line: #d9e2ec;12        --text: #172033;13        --muted: #637083;14        --accent: #116d6e;15        --accent-2: #204b8c;16        --danger: #b42318;17        --warn: #a15c07;18        --ok: #16794c;19      }20 21      * {22        box-sizing: border-box;23      }24 25      body {26        margin: 0;27        background: var(--bg);28        color: var(--text);29        font-family: Arial, Helvetica, sans-serif;30      }31 32      header {33        background: #0d1b2a;34        color: white;35        padding: 18px 24px;36      }37 38      header h1 {39        margin: 0;40        font-size: 22px;41        font-weight: 700;42      }43 44      header p {45        margin: 6px 0 0;46        color: #cbd5e1;47        font-size: 14px;48      }49 50      main {51        display: grid;52        grid-template-columns: minmax(360px, 1.05fr) minmax(360px, 0.95fr);53        gap: 16px;54        padding: 16px;55      }56 57      section {58        background: var(--panel);59        border: 1px solid var(--line);60        border-radius: 8px;61        min-width: 0;62      }63 64      .section-head {65        display: flex;66        justify-content: space-between;67        gap: 12px;68        align-items: center;69        padding: 14px 16px;70        border-bottom: 1px solid var(--line);71      }72 73      h2 {74        margin: 0;75        font-size: 17px;76      }77 78      .body {79        padding: 16px;80      }81 82      .row {83        display: grid;84        grid-template-columns: repeat(4, minmax(0, 1fr));85        gap: 10px;86        margin-bottom: 10px;87      }88 89      .api-row {90        display: grid;91        grid-template-columns: minmax(220px, 1.2fr) minmax(170px, 0.8fr) minmax(150px, 0.7fr);92        gap: 10px;93        margin-bottom: 10px;94      }95 96      label {97        display: block;98        color: var(--muted);99        font-size: 12px;100        margin-bottom: 4px;101      }102 103      input,104      select,105      textarea {106        width: 100%;107        border: 1px solid var(--line);108        border-radius: 6px;109        padding: 9px 10px;110        background: white;111        color: var(--text);112        font-size: 14px;113      }114 115      textarea {116        min-height: 320px;117        resize: vertical;118        font-family: Consolas, monospace;119        line-height: 1.35;120      }121 122      button {123        border: 0;124        border-radius: 6px;125        background: var(--accent);126        color: white;127        cursor: pointer;128        font-weight: 700;129        padding: 10px 13px;130      }131 132      button.secondary {133        background: var(--accent-2);134      }135 136      button.ghost {137        background: #e8eef6;138        color: var(--text);139      }140 141      button.danger {142        background: var(--danger);143      }144 145      .actions {146        display: flex;147        flex-wrap: wrap;148        gap: 8px;149        margin: 12px 0;150      }151 152      .decision {153        display: grid;154        grid-template-columns: repeat(4, minmax(0, 1fr));155        gap: 10px;156        margin-top: 12px;157      }158 159      .metric {160        border: 1px solid var(--line);161        border-radius: 8px;162        padding: 12px;163        background: #fbfdff;164      }165 166      .metric span {167        color: var(--muted);168        display: block;169        font-size: 12px;170      }171 172      .metric strong {173        display: block;174        font-size: 18px;175        margin-top: 5px;176        overflow-wrap: anywhere;177      }178 179      .STP {180        color: var(--ok);181      }182 183      .CONDITIONAL_STP {184        color: var(--warn);185      }186 187      .NON_STP,188      .REJECT {189        color: var(--danger);190      }191 192      pre {193        background: #101827;194        color: #dbeafe;195        border-radius: 8px;196        padding: 12px;197        overflow: auto;198        max-height: 360px;199        font-size: 13px;200      }201 202      table {203        width: 100%;204        border-collapse: collapse;205        font-size: 13px;206      }207 208      th,209      td {210        border-bottom: 1px solid var(--line);211        padding: 9px 7px;212        text-align: left;213        vertical-align: top;214      }215 216      th {217        color: var(--muted);218        font-weight: 700;219        background: #f8fafc;220        position: sticky;221        top: 0;222      }223 224      .table-wrap {225        border: 1px solid var(--line);226        border-radius: 8px;227        overflow: auto;228        max-height: 390px;229      }230 231      .status {232        min-height: 20px;233        color: var(--muted);234        font-size: 13px;235      }236 237      .status.ok {238        color: var(--ok);239      }240 241      .status.error {242        color: var(--danger);243      }244 245      .hint {246        color: var(--muted);247        font-size: 12px;248        line-height: 1.45;249        margin: 4px 0 12px;250      }251 252      .request-line {253        background: #eef6f6;254        border: 1px solid var(--line);255        border-radius: 6px;256        color: var(--accent);257        font-family: Consolas, monospace;258        font-size: 13px;259        margin: 8px 0 12px;260        overflow-wrap: anywhere;261        padding: 9px 10px;262      }263 264      .edit-banner {265        display: none;266        align-items: center;267        justify-content: space-between;268        gap: 10px;269        border: 1px solid #f2c94c;270        background: #fff8db;271        border-radius: 8px;272        padding: 10px 12px;273        margin-bottom: 12px;274        color: #6b4e00;275        font-size: 13px;276      }277 278      .rule-form {279        display: grid;280        grid-template-columns: repeat(3, minmax(0, 1fr));281        gap: 10px;282        margin-bottom: 12px;283      }284 285      .wide {286        grid-column: span 2;287      }288 289      @media (max-width: 980px) {290        main,291        .api-row,292        .row,293        .decision,294        .rule-form {295          grid-template-columns: 1fr;296        }297 298        .wide {299          grid-column: auto;300        }301      }302    </style>303  </head>304  <body>305    <header>306      <h1>Two Wheeler Loan STP Scorecard Console</h1>307      <p>Configure rules, simulate decisions, and generate persisted scorecards from one screen.</p>308    </header>309 310    <main>311      <section>312        <div class="section-head">313          <h2>Scorecard Tester</h2>314          <button class="ghost" onclick="loadSample()">Load Sample</button>315        </div>316        <div class="body">317          <div class="row">318            <div>319              <label>Tenant</label>320              <input id="tenantId" value="default" />321            </div>322            <div>323              <label>Product</label>324              <select id="productType" onchange="syncProductChange()">325                <option value="TWO_WHEELER_LOAN">Two Wheeler Loan</option>326                <option value="NEW_CAR_LOAN">New Car Loan</option>327                <option value="USED_CAR_LOAN">Used Car Loan</option>328                <option value="NEW_COMMERCIAL_VEHICLE_LOAN">New Commercial Vehicle</option>329                <option value="USED_COMMERCIAL_VEHICLE_LOAN">Used Commercial Vehicle</option>330                <option value="NEW_TRACTOR_LOAN">New Tractor Loan</option>331                <option value="USED_TRACTOR_LOAN">Used Tractor Loan</option>332              </select>333            </div>334            <div>335              <label>Lead ID</label>336              <input id="leadId" value="TW10001" />337            </div>338            <div>339              <label>Journey Trigger</label>340              <select id="dueDiligenceStatus">341                <option value="APPROVED">Due Diligence Approved</option>342                <option value="PENDING">Due Diligence Pending</option>343                <option value="REJECTED">Due Diligence Rejected</option>344              </select>345            </div>346          </div>347 348          <div class="api-row">349            <div>350              <label>API Base URL</label>351              <input id="apiBaseUrl" />352            </div>353            <div>354              <label>Endpoint</label>355              <select id="mode" onchange="refreshRequestPreview()">356                <option value="simulate-scorecard">POST /simulate-scorecard</option>357                <option value="generate-scorecard">POST /generate-scorecard</option>358              </select>359            </div>360            <div>361              <label>Method</label>362              <input id="method" value="POST" disabled />363            </div>364          </div>365          <div class="hint">366            Use simulate for testing only. Use generate after due diligence approval when you want the scorecard result saved against the lead.367          </div>368          <div class="request-line" id="requestPreview"></div>369 370          <label>Request JSON</label>371          <textarea id="payload"></textarea>372 373          <div class="actions">374            <button onclick="runScorecard()">Send API Request</button>375            <button class="secondary" onclick="triggerAfterDueDiligence()">Trigger After Due Diligence</button>376            <button class="secondary" onclick="getSavedResult()">Get Saved Result</button>377            <button class="ghost" onclick="openDocs()">Swagger Docs</button>378            <button class="ghost" onclick="copyCurl()">Copy cURL</button>379          </div>380          <div class="status" id="scoreStatus"></div>381 382          <div class="decision">383            <div class="metric">384              <span>Decision</span>385              <strong id="decision">-</strong>386            </div>387            <div class="metric">388              <span>Final Score</span>389              <strong id="finalScore">-</strong>390            </div>391            <div class="metric">392              <span>Journey Action</span>393              <strong id="journeyAction">-</strong>394            </div>395            <div class="metric">396              <span>Bureau Risk</span>397              <strong id="bureauRisk">-</strong>398            </div>399          </div>400 401          <pre id="scoreOutput">{}</pre>402          <label>Request Preview</label>403          <pre id="requestOutput">{}</pre>404        </div>405      </section>406 407      <section>408        <div class="section-head">409          <h2>Rule Configuration</h2>410          <button class="ghost" onclick="loadRules()">Refresh Rules</button>411        </div>412        <div class="body">413          <div class="rule-form">414            <input id="editingRuleId" type="hidden" />415            <div>416              <label>Rule Code</label>417              <input id="ruleCode" placeholder="CIBIL_SUPER_HIGH" />418            </div>419            <div class="wide">420              <label>Rule Name</label>421              <input id="ruleName" placeholder="CIBIL score above 800" />422            </div>423            <div>424              <label>Rule Type</label>425              <select id="ruleType">426                <option>SCORE</option>427                <option>HARD_REJECT</option>428                <option>NON_STP</option>429                <option>STP_ELIGIBILITY</option>430              </select>431            </div>432            <div>433              <label>Field Name</label>434              <input id="fieldName" placeholder="cibilScore" />435            </div>436            <div>437              <label>Operator</label>438              <select id="operator">439                <option>&gt;=</option>440                <option>&gt;</option>441                <option>&lt;=</option>442                <option>&lt;</option>443                <option>=</option>444                <option>!=</option>445                <option>BETWEEN</option>446                <option>IN</option>447                <option>NOT_IN</option>448              </select>449            </div>450            <div>451              <label>Value</label>452              <input id="fieldValue" placeholder="750 or 700,749" />453            </div>454            <div>455              <label>Score Value</label>456              <input id="scoreValue" placeholder="10" />457            </div>458            <div>459              <label>Score Component</label>460              <input id="scoreComponent" placeholder="cibil" />461            </div>462            <div>463              <label>Decision Action</label>464              <select id="decisionAction">465                <option value="">None</option>466                <option>SCORE</option>467                <option>STP</option>468                <option>UW</option>469                <option>REJECT</option>470                <option>NON_STP</option>471              </select>472            </div>473            <div>474              <label>Reason Code</label>475              <input id="reasonCode" placeholder="HIGH_CIBIL" />476            </div>477            <div>478              <label>Priority</label>479              <input id="priority" value="100" />480            </div>481          </div>482 483          <div class="edit-banner" id="editBanner">484            <span id="editText">Editing rule</span>485            <button class="ghost" onclick="cancelEdit()">Cancel Edit</button>486          </div>487 488          <div class="actions">489            <button id="saveRuleButton" onclick="saveRule()">Create Rule</button>490            <button class="ghost" onclick="clearRuleForm()">Clear</button>491            <button class="secondary" onclick="exportRules()">Export Product Rules</button>492          </div>493          <div class="status" id="ruleStatus"></div>494 495          <div class="table-wrap">496            <table>497              <thead>498                <tr>499                  <th>ID</th>500                  <th>Code</th>501                  <th>Type</th>502                  <th>Condition</th>503                  <th>Score/Action</th>504                  <th>Status</th>505              <th>Actions</th>506                </tr>507              </thead>508              <tbody id="rulesTable"></tbody>509            </table>510          </div>511        </div>512      </section>513    </main>514 515    <script>516      const samplePayload = {517        tenantId: "default",518        productType: "TWO_WHEELER_LOAN",519        application: {520          leadId: "TW10001",521          customerName: "ABC",522          mobile: "9999999999",523          employmentType: "SALARIED",524          monthlyIncome: 45000,525          existingEMI: 8000,526          foir: 32,527          loanAmount: 95000,528          assetCost: 125000,529          ltv: 76,530          tenure: 36,531          dealerId: "DL101",532          state: "Karnataka",533          city: "Bangalore",534          negativeArea: false,535          fraudFlag: false,536          faceMatchScore: 92,537          panVerified: true,538          aadhaarVerified: true,539          bankingSurrogate: true,540          coApplicant: false541        },542        bureau: {543          cibilScore: 753,544          creditVintageMonths: 123,545          activeAccounts: 2,546          closedAccounts: 47,547          overdueAmount: 0,548          writtenOffAccounts: 0,549          settledAccounts: 0,550          active90PlusDpd: false,551          maxDPD: 0,552          dpd30Count: 0,553          dpd60Count: 0,554          dpd90Count: 0,555          creditEnquiries6M: 2,556          creditEnquiries12M: 4,557          activeUnsecuredLoans: 1,558          activeSecuredLoans: 1,559          jointAccounts: 0,560          creditUtilization: 2561        }562      };563 564      function setStatus(id, message) {565        const element = document.getElementById(id);566        element.textContent = message;567        element.className = "status";568      }569 570      function setStatusState(id, message, state) {571        const element = document.getElementById(id);572        element.textContent = message;573        element.className = `status ${state || ""}`;574      }575 576      function loadSample() {577        document.getElementById("payload").value = JSON.stringify(samplePayload, null, 2);578        document.getElementById("leadId").value = samplePayload.application.leadId;579        refreshRequestPreview();580      }581 582      function syncProductChange() {583        try {584          const payload = JSON.parse(document.getElementById("payload").value);585          payload.productType = document.getElementById("productType").value;586          document.getElementById("payload").value = JSON.stringify(payload, null, 2);587        } catch (_) {588          // Keep user-entered JSON untouched if it is mid-edit.589        }590        cancelEdit();591        loadRules();592        refreshRequestPreview();593      }594 595      function syncHeaderFields(payload) {596        payload.tenantId = document.getElementById("tenantId").value || "default";597        payload.productType = document.getElementById("productType").value || "TWO_WHEELER_LOAN";598        payload.application.leadId = document.getElementById("leadId").value || payload.application.leadId;599        return payload;600      }601 602      function getBaseUrl() {603        return (document.getElementById("apiBaseUrl").value || window.location.origin).replace(/\/$/, "");604      }605 606      function buildUrl(path) {607        return `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;608      }609 610      function getScorecardPath() {611        return `/${document.getElementById("mode").value}`;612      }613 614      function refreshRequestPreview() {615        const path = getScorecardPath();616        document.getElementById("requestPreview").textContent = `POST ${buildUrl(path)}`;617      }618 619      async function api(path, options = {}) {620        const response = await fetch(buildUrl(path), {621          headers: { "Content-Type": "application/json", ...(options.headers || {}) },622          ...options623        });624        const text = await response.text();625        let body = null;626        try {627          body = text ? JSON.parse(text) : null;628        } catch (_) {629          body = text;630        }631        if (!response.ok) {632          throw new Error(body?.detail || body || response.statusText);633        }634        return { body, status: response.status, statusText: response.statusText };635      }636 637      function readPayload() {638        const payload = syncHeaderFields(JSON.parse(document.getElementById("payload").value));639        document.getElementById("payload").value = JSON.stringify(payload, null, 2);640        return payload;641      }642 643      function renderRequest(path, payload) {644        const request = {645          method: "POST",646          url: buildUrl(path),647          headers: { "Content-Type": "application/json" },648          body: payload649        };650        document.getElementById("requestOutput").textContent = JSON.stringify(request, null, 2);651      }652 653      function renderScore(body) {654        document.getElementById("scoreOutput").textContent = JSON.stringify(body, null, 2);655        const decision = document.getElementById("decision");656        decision.textContent = body.decision || "-";657        decision.className = body.decision || "";658        document.getElementById("finalScore").textContent = body.finalScore ?? "-";659        document.getElementById("journeyAction").textContent = body.journeyAction || "-";660        document.getElementById("bureauRisk").textContent = body.bureauRisk || "-";661      }662 663      async function runScorecard() {664        try {665          setStatus("scoreStatus", "Sending API request...");666          const payload = readPayload();667          const path = getScorecardPath();668          renderRequest(path, payload);669          const result = await api(path, { method: "POST", body: JSON.stringify(payload) });670          renderScore(result.body);671          setStatusState("scoreStatus", `HTTP ${result.status} ${result.statusText} - scorecard completed.`, "ok");672        } catch (error) {673          setStatusState("scoreStatus", `Error: ${error.message}`, "error");674        }675      }676 677      async function triggerAfterDueDiligence() {678        const dueDiligenceStatus = document.getElementById("dueDiligenceStatus").value;679        if (dueDiligenceStatus !== "APPROVED") {680          setStatusState(681            "scoreStatus",682            `API not triggered because due diligence is ${dueDiligenceStatus}. Select Approved to call the scorecard.`,683            "error"684          );685          return;686        }687        document.getElementById("mode").value = "generate-scorecard";688        refreshRequestPreview();689        await runScorecard();690      }691 692      async function getSavedResult() {693        try {694          const leadId = document.getElementById("leadId").value;695          setStatus("scoreStatus", "Fetching saved result...");696          const result = await api(`/scorecard/${encodeURIComponent(leadId)}`);697          renderScore(result.body);698          setStatusState("scoreStatus", `HTTP ${result.status} ${result.statusText} - saved result loaded.`, "ok");699        } catch (error) {700          setStatusState("scoreStatus", `Error: ${error.message}`, "error");701        }702      }703 704      async function loadRules() {705        try {706          setStatus("ruleStatus", "Loading rules...");707          const tenant = encodeURIComponent(document.getElementById("tenantId").value || "default");708          const product = encodeURIComponent(document.getElementById("productType").value || "TWO_WHEELER_LOAN");709          const result = await api(`/rules?tenant_id=${tenant}&product_type=${product}`);710          const rules = result.body;711          const rows = rules.map(rule => `712            <tr>713              <td>${rule.id}</td>714              <td>${rule.rule_code}<br><small>${rule.reason_code}</small></td>715              <td>${rule.rule_type}</td>716              <td>${rule.field_name} ${rule.operator} ${rule.field_value}</td>717              <td>${rule.score_value ?? ""} ${rule.decision_action ?? ""}</td>718              <td>${rule.is_active ? "Active" : "Disabled"}</td>719              <td>720                <button class="ghost" onclick='editRule(${JSON.stringify(rule)})'>Edit</button>721                <button class="${rule.is_active ? "danger" : "secondary"}" onclick="toggleRule(${rule.id}, ${!rule.is_active})">722                  ${rule.is_active ? "Disable" : "Enable"}723                </button>724              </td>725            </tr>726          `).join("");727          document.getElementById("rulesTable").innerHTML = rows;728          setStatus("ruleStatus", `${rules.length} rules loaded.`);729        } catch (error) {730          setStatus("ruleStatus", `Error: ${error.message}`);731        }732      }733 734      function buildRulePayload() {735        const scoreValue = document.getElementById("scoreValue").value;736        return {737          tenant_id: document.getElementById("tenantId").value || "default",738          product_type: document.getElementById("productType").value || "TWO_WHEELER_LOAN",739          rule_code: document.getElementById("ruleCode").value,740          rule_name: document.getElementById("ruleName").value,741          rule_type: document.getElementById("ruleType").value,742          field_name: document.getElementById("fieldName").value,743          operator: document.getElementById("operator").value,744          field_value: document.getElementById("fieldValue").value,745          score_value: scoreValue ? Number(scoreValue) : null,746          score_component: document.getElementById("scoreComponent").value || null,747          decision_action: document.getElementById("decisionAction").value || null,748          reason_code: document.getElementById("reasonCode").value,749          priority: Number(document.getElementById("priority").value || 100),750          is_active: true751        };752      }753 754      async function saveRule() {755        const editingRuleId = document.getElementById("editingRuleId").value;756        if (editingRuleId) {757          await updateRule(editingRuleId);758        } else {759          await createRule();760        }761      }762 763      async function createRule() {764        try {765          setStatus("ruleStatus", "Creating rule...");766          const payload = buildRulePayload();767          await api("/rules", { method: "POST", body: JSON.stringify(payload) });768          clearRuleForm();769          await loadRules();770          setStatus("ruleStatus", "Rule created.");771        } catch (error) {772          setStatus("ruleStatus", `Error: ${error.message}`);773        }774      }775 776      async function updateRule(ruleId) {777        try {778          setStatus("ruleStatus", "Updating rule...");779          const payload = buildRulePayload();780          delete payload.tenant_id;781          delete payload.product_type;782          delete payload.rule_code;783          delete payload.rule_type;784          payload.actor = "admin-ui";785          await api(`/rules/${ruleId}`, { method: "PUT", body: JSON.stringify(payload) });786          clearRuleForm();787          await loadRules();788          setStatus("ruleStatus", "Rule updated.");789        } catch (error) {790          setStatus("ruleStatus", `Error: ${error.message}`);791        }792      }793 794      function editRule(rule) {795        document.getElementById("editingRuleId").value = rule.id;796        document.getElementById("ruleCode").value = rule.rule_code || "";797        document.getElementById("ruleName").value = rule.rule_name || "";798        document.getElementById("ruleType").value = rule.rule_type || "SCORE";799        document.getElementById("fieldName").value = rule.field_name || "";800        document.getElementById("operator").value = rule.operator || "=";801        document.getElementById("fieldValue").value = rule.field_value || "";802        document.getElementById("scoreValue").value = rule.score_value ?? "";803        document.getElementById("scoreComponent").value = rule.score_component || "";804        document.getElementById("decisionAction").value = rule.decision_action || "";805        document.getElementById("reasonCode").value = rule.reason_code || "";806        document.getElementById("priority").value = rule.priority || 100;807        document.getElementById("saveRuleButton").textContent = "Save Rule Changes";808        document.getElementById("editText").textContent = `Editing #${rule.id} - ${rule.rule_code}`;809        document.getElementById("editBanner").style.display = "flex";810        setStatus("ruleStatus", "Rule loaded into form. Change values and save.");811        window.scrollTo({ top: 0, behavior: "smooth" });812      }813 814      async function toggleRule(id, isActive) {815        try {816          await api(`/rules/${id}/status`, {817            method: "PATCH",818            body: JSON.stringify({ is_active: isActive, actor: "admin-ui" })819          });820          await loadRules();821        } catch (error) {822          setStatus("ruleStatus", `Error: ${error.message}`);823        }824      }825 826      function clearRuleForm() {827        [828          "ruleCode",829          "ruleName",830          "fieldName",831          "fieldValue",832          "scoreValue",833          "scoreComponent",834          "reasonCode"835        ].forEach(id => document.getElementById(id).value = "");836        document.getElementById("priority").value = "100";837        cancelEdit();838      }839 840      function cancelEdit() {841        document.getElementById("editingRuleId").value = "";842        document.getElementById("saveRuleButton").textContent = "Create Rule";843        document.getElementById("editBanner").style.display = "none";844      }845 846      async function exportRules() {847        try {848          const tenant = encodeURIComponent(document.getElementById("tenantId").value || "default");849          const product = encodeURIComponent(document.getElementById("productType").value || "TWO_WHEELER_LOAN");850          const result = await api(`/rules?tenant_id=${tenant}&product_type=${product}`);851          const rules = result.body;852          const blob = new Blob([JSON.stringify(rules, null, 2)], { type: "application/json" });853          const url = URL.createObjectURL(blob);854          const link = document.createElement("a");855          link.href = url;856          link.download = `${product}_rules.json`;857          link.click();858          URL.revokeObjectURL(url);859          setStatus("ruleStatus", "Rules exported.");860        } catch (error) {861          setStatus("ruleStatus", `Error: ${error.message}`);862        }863      }864 865      function openDocs() {866        window.open(buildUrl("/docs"), "_blank");867      }868 869      async function copyCurl() {870        try {871          const payload = readPayload();872          const path = getScorecardPath();873          renderRequest(path, payload);874          const curl = [875            `curl -X POST "${buildUrl(path)}"`,876            `-H "Content-Type: application/json"`,877            `-d '${JSON.stringify(payload)}'`878          ].join(" ");879          await navigator.clipboard.writeText(curl);880          setStatusState("scoreStatus", "cURL copied.", "ok");881        } catch (error) {882          setStatusState("scoreStatus", `Error: ${error.message}`, "error");883        }884      }885 886      document.getElementById("apiBaseUrl").value = window.location.origin;887      document.getElementById("apiBaseUrl").addEventListener("input", refreshRequestPreview);888      loadSample();889      loadRules();890    </script>891  </body>892</html>893