CLAIMS, MADE CLEAR
+Know what to fix before you submit.
+ClaimClear reviews your clinical record and draft claim in one calm, guided pass-so avoidable errors do not become delays.
+diff --git a/apps/extraction-service/llm_extract.py b/apps/extraction-service/llm_extract.py index 2467d20..7964668 100644 --- a/apps/extraction-service/llm_extract.py +++ b/apps/extraction-service/llm_extract.py @@ -1,5 +1,6 @@ import json import os +import time from pathlib import Path from dotenv import load_dotenv @@ -7,10 +8,22 @@ from google import genai from google.genai import types +from groq import Groq # pip install groq + env_path = Path(__file__).resolve().parent / ".env" load_dotenv(env_path) GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") +GROQ_API_KEY = os.getenv("GROQ_API_KEY") # get a free key at https://console.groq.com/keys + +# Fallback model config +GROQ_MODEL = "openai/gpt-oss-120b" # llama-3.3-70b-versatile was deprecated by Groq in June 2026 +GEMINI_MAX_RETRIES = 2 # retries before falling back +GEMINI_RETRY_DELAY_SECONDS = 2 # backoff between retries +GROQ_MAX_RETRIES = 2 # retries before falling back to Ollama +GROQ_RETRY_DELAY_SECONDS = 2 + +groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None # ---------------- Discharge Summary schema ---------------- @@ -213,7 +226,8 @@ class ExtractionOutput(BaseModel): Rules: - If a field cannot be found, use null (or false/0 for booleans/numbers where structurally required) -- do not guess or fabricate values. - Dates must be YYYY-MM-DD, times HH:MM (24-hour). -- Return both objects nested under top-level keys "discharge_summary" and "claim_part_b". +- Return ONLY valid JSON with both objects nested under top-level keys "discharge_summary" and "claim_part_b". +- Do not include any explanation, markdown formatting, or code fences -- raw JSON only. CLINICAL DOCUMENT TEXT (discharge summary source): \"\"\" @@ -229,25 +243,99 @@ class ExtractionOutput(BaseModel): client = genai.Client(api_key=GEMINI_API_KEY) +def _call_gemini(prompt: str) -> dict: + response = client.models.generate_content( + model="gemini-3.6-flash", + contents=prompt, + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=ExtractionOutput, + temperature=0.0, + ), + ) + return json.loads(response.text) + + +def _strip_code_fences(text: str) -> str: + text = text.strip() + if text.startswith("```"): + # remove ```json ... ``` or ``` ... ``` + text = text.split("```", 2)[1] if text.count("```") >= 2 else text + if text.lstrip().startswith("json"): + text = text.lstrip()[4:] + return text.strip().strip("`").strip() + + +def _call_groq(prompt: str) -> dict: + if groq_client is None: + raise RuntimeError("GROQ_API_KEY not set in .env") + + response = groq_client.chat.completions.create( + model="llama-3.3-70b-versatileL", + messages=[{"role": "user", "content": prompt}], + temperature=0.0, + response_format={"type": "json_object"}, + ) + raw = response.choices[0].message.content + raw = _strip_code_fences(raw) + return json.loads(raw) + + def run(clinical_text: str, claim_text: str) -> dict: prompt = PROMPT_TEMPLATE.format(clinical_text=clinical_text, claim_text=claim_text) - try: - response = client.models.generate_content( - model="gemini-3.6-flash", - contents=prompt, - config=types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=ExtractionOutput, - temperature=0.0, - ), + result = None + gemini_error = None + groq_error = None + + # --- Tier 1: Gemini --- + for attempt in range(1, GEMINI_MAX_RETRIES + 1): + try: + result = _call_gemini(prompt) + break + except Exception as e: + gemini_error = e + print(f"[Gemini] attempt {attempt}/{GEMINI_MAX_RETRIES} failed: {e}") + if attempt < GEMINI_MAX_RETRIES: + time.sleep(GEMINI_RETRY_DELAY_SECONDS * attempt) + + # --- Tier 2: Groq --- + if result is None: + print(f"[Gemini] exhausted retries ({gemini_error}). Trying Groq '{GROQ_MODEL}'...") + for attempt in range(1, GROQ_MAX_RETRIES + 1): + try: + result = _call_groq(prompt) + break + except Exception as e: + groq_error = e + print(f"[Groq] attempt {attempt}/{GROQ_MAX_RETRIES} failed: {e}") + if attempt < GROQ_MAX_RETRIES: + time.sleep(GROQ_RETRY_DELAY_SECONDS * attempt) + + # --- Both providers failed --- + if result is None: + raise RuntimeError( + f"All providers failed.\n" + f"Gemini error: {gemini_error}\n" + f"Groq error: {groq_error}" ) - except Exception as e: - raise RuntimeError(f"Gemini API call failed: {e}") from e - result = json.loads(response.text) + # --- Validate against schema regardless of source --- + try: + validated = ExtractionOutput(**result) + result = validated.model_dump() + except Exception as e: + raise RuntimeError(f"Extraction output failed schema validation: {e}") from e return { "discharge_summary": result["discharge_summary"], "claim_part_b": result["claim_part_b"], - } \ No newline at end of file + } + + +if __name__ == "__main__": + # quick manual test + sample_clinical = "Patient Name: John Doe. Admitted 2026-01-01, discharged 2026-01-05..." + sample_claim = "Hospital: ABC Hospital. IP Reg No: 12345..." + out = run(sample_clinical, sample_claim) + print(json.dumps(out, indent=2)) \ No newline at end of file diff --git a/apps/extraction-service/rules_engine.py b/apps/extraction-service/rules_engine.py index 2ce5b36..15e1f3c 100644 --- a/apps/extraction-service/rules_engine.py +++ b/apps/extraction-service/rules_engine.py @@ -104,7 +104,10 @@ def check_rule_03_diagnosis_match(clinical: Dict[str, Any], claim: Dict[str, Any if not claim_icd: return True, "Primary ICD-10 diagnosis code is completely missing from Claim Form Part B." - if clinical_icd and (clinical_icd != claim_icd): + if not clinical_icd: + return True, "Cannot verify diagnosis: Discharge Summary is missing a primary ICD-10 code to cross-check against the claim." + + if clinical_icd != claim_icd: return True, f"Diagnosis mismatch: Claim states ICD '{claim_icd}', but Discharge Summary specifies '{clinical_icd}'." return False, "" @@ -121,6 +124,12 @@ def check_rule_04_min_24hr_stay(clinical: Dict[str, Any], claim: Dict[str, Any]) duration_hours = (dis - adm).total_seconds() / 3600.0 + # If duration is negative, the dates are inverted -- that's a data integrity + # problem already caught and reported by RULE_02_DATE_CHRONOLOGY. Don't stack + # a second, nonsensical "-X hours" finding for the same root cause. + if duration_hours < 0: + return False, "" + if duration_hours < 24.0 and "day" not in admission_type: return True, f"Inpatient stay was {duration_hours:.1f} hours (< 24 hours) but not marked as Day Care." except Exception: @@ -161,6 +170,23 @@ def check_rule_08_hospital_id(clinical: Dict[str, Any], claim: Dict[str, Any]) - return False, "" +def check_rule_18_bank_details(clinical: Dict[str, Any], claim: Dict[str, Any]) -> Tuple[bool, str]: + """RULE_18: Ensures insured's bank account number and IFSC code are present for NEFT settlement.""" + bank_details = claim.get("section_f_bank_details", {}) or {} + account_no = bank_details.get("account_number") + ifsc_code = bank_details.get("ifsc_code") + + missing = [] + if not account_no or not str(account_no).strip(): + missing.append("account number") + if not ifsc_code or not str(ifsc_code).strip(): + missing.append("IFSC code") + + if missing: + return True, f"Insured's bank {' and '.join(missing)} missing from Claim Form Part A Section F -- required for NEFT settlement." + return False, "" + + def check_rule_16_arithmetic(clinical: Dict[str, Any], claim: Dict[str, Any]) -> Tuple[bool, str]: """RULE_16: Verifies itemized line items sum up to total_claimed_amount.""" finances = claim.get("section_e_financial_summary", {}) or {} @@ -195,6 +221,7 @@ def check_rule_16_arithmetic(clinical: Dict[str, Any], claim: Dict[str, Any]) -> ("RULE_07_DOCTOR_REG_MISSING", check_rule_07_doctor_reg), ("RULE_08_HOSPITAL_ID_MISSING", check_rule_08_hospital_id), ("RULE_16_ARITHMETIC_TOTAL_MISMATCH", check_rule_16_arithmetic), + ("RULE_18_BANK_DETAILS_MISSING", check_rule_18_bank_details), ] diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 98b0764..8df379a 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -6,7 +6,8 @@ --color-background: var(--paper); --color-foreground: var(--ink); --color-border: var(--line); - --font-sans: var(--font-inter-tight); + --font-sans: var(--font-montserrat); + --font-serif: var(--font-playfair); --font-mono: var(--font-plex-mono); } @@ -24,6 +25,8 @@ --foreground: var(--ink); --border: var(--line); --radius: 0; + --radius-btn: 14px; + --radius-card: 26px; } :root[data-theme='light'] { @@ -45,29 +48,33 @@ * { box-sizing: border-box; } html { background: var(--paper); } -body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--font-inter-tight), Arial, sans-serif; font-size: 16px; line-height: 1.5; } +body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--font-montserrat), Arial, sans-serif; font-size: 16px; line-height: 1.5; } a { color: inherit; text-decoration: none; } button, input { font: inherit; } :focus-visible { outline: 2px solid var(--teal); outline-offset: 4px; } .shell { width: min(100% - 48px, 1120px); margin: 0 auto; } -.site-header { display: flex; align-items: center; justify-content: space-between; padding: 26px 0; border-bottom: 1px solid var(--line); } -.header-actions { display: flex; align-items: center; gap: 18px; } -.wordmark { font-size: 20px; font-weight: 700; letter-spacing: -0.04em; } -.theme-toggle { display: inline-flex; align-items: center; gap: 9px; border: 1px solid var(--line); background: transparent; color: var(--ink); padding: 8px 11px; cursor: pointer; font: 12px var(--font-plex-mono), monospace; } -.theme-toggle-track { display: inline-flex; align-items: center; width: 28px; height: 16px; padding: 2px; border: 1px solid var(--muted); } -.theme-toggle-thumb { width: 10px; height: 10px; background: var(--teal); transform: translateX(10px); transition: transform 160ms ease; } +.site-header { display: flex; align-items: center; justify-content: space-between; padding: 22px 0; } +.header-actions { display: flex; align-items: center; gap: 14px; } +.wordmark { font-family: var(--font-playfair), serif; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; } +.theme-toggle { display: inline-flex; align-items: center; gap: 9px; border: 1px solid var(--line); border-radius: var(--radius-btn); background: transparent; color: var(--ink); padding: 8px 12px; cursor: pointer; font: 12px var(--font-plex-mono), monospace; } +.theme-toggle-track { display: inline-flex; align-items: center; width: 28px; height: 16px; padding: 2px; border: 1px solid var(--muted); border-radius: 999px; } +.theme-toggle-thumb { width: 10px; height: 10px; background: var(--teal); border-radius: 999px; transform: translateX(10px); transition: transform 160ms ease; } :root[data-theme='light'] .theme-toggle-thumb { transform: translateX(0); } -.button { display: inline-flex; align-items: center; gap: 18px; border: 1px solid var(--navy); background: var(--navy); color: var(--paper); padding: 14px 20px; font-weight: 600; transition: background 160ms ease, color 160ms ease; } +.button { display: inline-flex; align-items: center; gap: 18px; border: 1px solid var(--navy); border-radius: var(--radius-btn); background: var(--navy); color: var(--paper); padding: 14px 22px; font-weight: 600; transition: background 160ms ease, color 160ms ease; } .button:hover { background: var(--paper); color: var(--navy); } -.button-small { padding: 9px 14px; font-size: 14px; } +.button-small { padding: 9px 16px; font-size: 14px; } .button:disabled { border-color: var(--line); background: #dfded9; color: #92928e; cursor: not-allowed; } +.button-invert { border-color: var(--paper); background: var(--paper); color: var(--navy); } +.button-invert:hover { background: var(--navy); color: var(--paper); } .hero { padding: 104px 0 128px; } .mono-label, .mono { font-family: var(--font-plex-mono), monospace; } .mono-label { color: var(--teal); font-size: 12px; margin: 0 0 22px; } h1, h2, p { margin-top: 0; } -h1 { max-width: 760px; margin-bottom: 26px; font-size: clamp(3rem, 7vw, 6.5rem); line-height: .95; letter-spacing: -.07em; font-weight: 650; } +h1, h2 { font-family: var(--font-playfair), serif; } +.accent-italic { font-style: italic; font-weight: 500; } +h1 { max-width: 760px; margin-bottom: 26px; font-size: clamp(3rem, 7vw, 6.5rem); line-height: .98; letter-spacing: -.01em; font-weight: 650; } .hero-copy { max-width: 530px; margin-bottom: 32px; font-size: 19px; color: #454b4c; line-height: 1.55; } .comparison { display: grid; grid-template-columns: repeat(2, 1fr); border-top: 1px solid var(--line); } .ladder { padding: 42px 42px 54px 0; } @@ -80,7 +87,7 @@ h1 { max-width: 760px; margin-bottom: 26px; font-size: clamp(3rem, 7vw, 6.5rem); .ladder-teal li::before { background: var(--teal); } .checks { display: grid; grid-template-columns: 1fr 2fr; gap: 48px; border-top: 1px solid var(--line); padding-top: 44px; padding-bottom: 110px; } .checks p { max-width: 620px; color: #454b4c; font-size: 19px; line-height: 1.6; } -.site-footer { border-top: 1px solid var(--line); padding: 30px 0 38px; display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: 28px; color: var(--muted); font-size: 13px; } .footer-brand, .footer-nav a, .footer-legal { display: flex; align-items: center; gap: 10px; } .footer-brand .wordmark { display: block; color: var(--ink); } .footer-brand p, .footer-legal { margin: 5px 0 0; line-height: 1.5; } .footer-mark { display: grid; place-items: center; width: 32px; height: 32px; border: 1px solid var(--line); color: var(--teal); } .footer-nav { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; } .footer-nav a:hover { color: var(--ink); } .footer-legal { align-items: flex-start; justify-content: flex-end; text-align: right; } +.site-footer { border-top: 1px solid var(--line); padding: 44px 0 48px; display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: 28px; color: var(--muted); font-size: 13px; } .footer-brand, .footer-nav a, .footer-legal { display: flex; align-items: center; gap: 10px; } .footer-brand .wordmark { display: block; color: var(--ink); } .footer-brand p, .footer-legal { margin: 6px 0 0; line-height: 1.5; } .footer-mark { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--line); border-radius: var(--radius-btn); color: var(--teal); } .footer-nav { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; } .footer-nav a:hover { color: var(--ink); } .footer-legal { align-items: flex-start; justify-content: flex-end; text-align: right; } .page-main { min-height: calc(100vh - 78px); padding: 72px 0 100px; } .page-intro { margin-bottom: 48px; } @@ -126,73 +133,91 @@ h1 { max-width: 760px; margin-bottom: 26px; font-size: clamp(3rem, 7vw, 6.5rem); .finding p { margin: 0 0 12px; color: #59605f; } .finding .fix { color: var(--ink); margin: 0; } -.landing-hero { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(320px, .75fr); gap: 84px; align-items: center; padding: 92px 0 96px; } -.hero-content h1 { max-width: 700px; margin-bottom: 24px; font-size: clamp(3.5rem, 7vw, 6.8rem); } -.hero-copy { max-width: 560px; margin-bottom: 28px; color: var(--muted); font-size: 20px; } +.landing-hero { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(340px, .9fr); gap: 64px; align-items: center; padding: 84px 0 100px; } +.hero-content h1 { max-width: 620px; margin-bottom: 22px; font-size: clamp(3.1rem, 6vw, 5.6rem); } +.hero-copy { max-width: 480px; margin-bottom: 30px; color: var(--muted); font-size: 19px; } .hero-actions { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; } .action-note { color: var(--muted); font: 12px var(--font-plex-mono), monospace; } -.hero-card { border: 1px solid var(--line); padding: 24px; background: color-mix(in srgb, var(--ink) 4%, var(--paper)); } -.hero-card-top { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--line); padding-bottom: 18px; color: var(--muted); font-size: 11px; } -.status-dot { color: var(--teal); } -.status-dot::before { content: ''; display: inline-block; width: 6px; height: 6px; margin-right: 7px; background: var(--teal); border-radius: 50%; } -.hero-card-score { display: flex; align-items: center; gap: 18px; padding: 30px 0; } -.hero-card-score span { color: var(--teal); font-size: clamp(3.6rem, 6vw, 5.2rem); line-height: .85; letter-spacing: -.09em; font-weight: 700; white-space: nowrap; } -.hero-card-score p { margin: 0; color: var(--ink); font-size: 16px; line-height: 1.35; } - .claim-stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); border-top: 1px solid var(--line); border-left: 1px solid var(--line); } - .claim-stat-grid div { display: flex; flex-direction: column; gap: 5px; min-height: 76px; padding: 13px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); } - .claim-stat-grid strong { color: var(--ink); font-size: 22px; letter-spacing: -.04em; } - .claim-stat-grid span { color: var(--muted); font: 10px var(--font-plex-mono), monospace; line-height: 1.35; text-transform: uppercase; } - .hero-card-list { border-top: 1px solid var(--line); } -.hero-card-list div { display: flex; gap: 11px; border-bottom: 1px solid var(--line); padding: 13px 0; color: var(--muted); font-size: 14px; } -.check-mark { color: var(--teal); font-weight: 700; } -.hero-card-foot { margin: 20px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; } -.reassurance { display: grid; grid-template-columns: 1fr 2fr; gap: 48px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: 24px 0; color: var(--muted); } -.reassurance p { max-width: 560px; margin: 0; color: var(--ink); font-size: 18px; } -.how-it-works { padding: 94px 0; } -.section-heading h2 { margin: 0; font-size: clamp(2.5rem, 5vw, 4.5rem); letter-spacing: -.07em; } -.step-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; margin-top: 46px; border: 1px solid var(--line); background: var(--line); } -.step-card { min-height: 210px; padding: 24px; background: var(--paper); } -.step-card .step-number { display: block; margin-bottom: 48px; color: var(--teal); } -.step-card h3 { margin: 0 0 8px; font-size: 21px; } -.step-card p { max-width: 220px; margin: 0; color: var(--muted); } -.final-cta { display: flex; align-items: end; justify-content: space-between; gap: 32px; border-top: 1px solid var(--line); padding: 72px 0 92px; } -.final-cta h2 { max-width: 650px; margin: 0; font-size: clamp(2.5rem, 5vw, 5rem); line-height: .98; letter-spacing: -.07em; } -.final-cta .mono-label { margin-bottom: 18px; } -.flow-steps { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 46px; } .flow-step-wrap { display: flex; align-items: center; } .flow-step { flex: 1; min-height: 220px; border: 1px solid var(--line); padding: 24px; background: var(--paper); } .flow-icon { display: grid; place-items: center; width: 44px; height: 44px; color: var(--teal); border: 1px solid var(--line); margin-bottom: 22px; } .flow-number { display: block; color: var(--muted); margin-bottom: 10px; font-size: 11px; } .flow-step h3 { margin: 0 0 8px; font-size: 21px; } .flow-step p { margin: 0; color: var(--muted); max-width: 190px; } .flow-arrow { flex: 0 0 24px; margin: 0 -12px; z-index: 1; color: var(--teal); background: var(--paper); } .dropzone { min-height: 270px; border: 1px solid var(--line); border-radius: 18px; padding: 28px; cursor: pointer; transition: border-color 160ms ease, background 160ms ease, transform 160ms ease; } .dropzone:hover, .dropzone.is-dragging { border-color: var(--teal); background: color-mix(in srgb, var(--teal) 7%, var(--paper)); transform: translateY(-2px); } .upload-icon { display: grid; place-items: center; width: 48px; height: 48px; border: 1px solid var(--line); border-radius: 12px; color: var(--teal); } .dropzone-top { align-items: flex-start; } .dropzone h2 { margin-top: 28px; } .dropzone p { line-height: 1.7; } .dropzone p small { font-size: 12px; color: var(--muted); } .browse { color: var(--ink); font-weight: 600; } .file-name { display: inline-flex; align-items: center; gap: 7px; } .upload-note { display: flex; align-items: center; gap: 8px; color: var(--muted); font: 12px var(--font-plex-mono), monospace; margin-bottom: 24px; } .page-main:has(.results-head) { display: grid; grid-template-columns: minmax(280px, .7fr) minmax(0, 1.3fr); gap: 64px; align-items: start; } .results-head { display: block; align-items: center; padding-bottom: 0; border-bottom: 0; position: sticky; top: 24px; } .score-panel { min-width: 330px; } .score-circle { display: flex; align-items: baseline; justify-content: center; width: 190px; height: 190px; border: 10px solid var(--amber); border-radius: 50%; padding-top: 4px; } .score-circle strong { font-size: 68px; line-height: 1; letter-spacing: -.08em; } .score-circle span { color: var(--muted); font: 13px var(--font-plex-mono), monospace; margin-left: 3px; } .score-status { margin: 18px 0 20px; } .score-warning { display: flex; align-items: flex-start; gap: 9px; max-width: 320px; color: var(--muted); font-size: 13px; line-height: 1.45; } .score-warning svg { flex: 0 0 auto; color: var(--rust); margin-top: 2px; } .score-warning strong { color: var(--rust); } .report-meta { align-self: flex-end; text-align: right; color: var(--muted); font-size: 13px; } .report-meta p { margin: 0 0 8px; } .findings { min-width: 0; } .findings-heading { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 30px; } .findings h2 { margin: 0; } .findings h2 span { color: var(--muted); font-weight: 400; } .findings-note { margin: 0; color: var(--muted); font-size: 13px; } .finding { display: grid; grid-template-columns: 28px 1fr; gap: 14px; padding-left: 16px; } .finding-icon { color: var(--amber); padding-top: 2px; } .finding-critical .finding-icon { color: var(--rust); } .finding-minor .finding-icon { color: var(--teal); } .finding h3 { margin-bottom: 7px; } .center-page { min-height: auto; padding: 84px 24px 100px; } .processing { width: min(100%, 560px); } .processing h1 { margin-bottom: 38px; } .site-footer { margin-top: auto; } +.hero-card { display: flex; flex-direction: column; gap: 16px; } +.hero-card-top { display: flex; justify-content: space-between; align-items: center; color: var(--muted); font-size: 11px; } +.status-dot { display: inline-flex; align-items: center; color: var(--muted); } +.status-dot::before { content: ''; display: inline-block; width: 6px; height: 6px; margin-right: 7px; background: var(--rust); border-radius: 50%; animation: live-blink 1.6s ease-in-out infinite; } +@keyframes live-blink { 0%, 100% { opacity: 1; } 50% { opacity: .2; } } -.report-page { background: #0b0f12; min-height: 100vh; } -.report-shell { padding: 38px 0 72px; } -.report-summary { display: flex; align-items: center; gap: 34px; padding: 4px 34px 34px; border-bottom: 1px solid #202831; } -.score-ring strong { color: var(--amber); font-size: 32px; line-height: 1; letter-spacing: -.06em; } -.score-ring span { margin-top: 6px; color: #c1c8cb; font: 10px var(--font-plex-mono), monospace; letter-spacing: .04em; } -.score-copy .mono-label { margin-bottom: 4px; color: #d7dee0; font-size: 11px; font-weight: 700; letter-spacing: .08em; } -.score-copy h1 { margin: 0 0 8px; color: #f2f5f5; font-size: 40px; letter-spacing: -.04em; } -.score-copy > p:not(.mono-label) { max-width: 560px; margin: 0; color: #c1c8cb; font-size: 14px; } -.severity-counts { display: flex; gap: 18px; margin-top: 12px; font: 11px var(--font-plex-mono), monospace; } -.severity-counts span { display: inline-flex; align-items: center; gap: 6px; } -.severity-counts i { width: 7px; height: 7px; border-radius: 50%; } -.count-critical { color: #ff6c73; }.count-critical i { background: #ff6c73; }.count-warning { color: #f3ad15; }.count-warning i { background: #f3ad15; }.count-info { color: #22b9ef; }.count-info i { background: #22b9ef; } -.report-findings { padding: 16px 14px 0; } -.report-section-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }.report-section-heading .mono-label { margin: 0; color: #d7dee0; font-size: 11px; font-weight: 700; letter-spacing: .08em; }.report-id { margin: 0; color: #7f8b91; font-size: 10px; } -.report-finding-list { display: grid; gap: 8px; } -.report-finding { overflow: hidden; border: 1px solid #202832; border-radius: 8px; background: #101419; }.report-finding.is-open { border-color: #72551a; background: #171b21; } -.report-finding-trigger { display: grid; grid-template-columns: 72px minmax(0, 1fr) 18px; align-items: center; gap: 12px; width: 100%; padding: 13px 16px; border: 0; background: transparent; color: #eef2f1; text-align: left; cursor: pointer; }.report-finding-trigger:hover { background: #171b21; } -.severity-badge { display: inline-flex; align-items: center; gap: 5px; width: max-content; padding: 3px 7px; border: 1px solid currentColor; border-radius: 3px; font: 9px var(--font-plex-mono), monospace; font-weight: 700; letter-spacing: .05em; }.critical .severity-badge { color: #ff6068; background: #241519; }.warning .severity-badge { color: #f3ad15; background: #2a2416; }.info .severity-badge { color: #22b9ef; background: #13252d; } -.finding-title { display: grid; gap: 2px; min-width: 0; }.finding-title .mono { overflow: hidden; color: #d4dadd; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }.finding-title strong { color: #edf1f2; font-size: 13px; }.finding-chevron { color: #68747a; transition: transform 160ms ease; }.is-open .finding-chevron { transform: rotate(180deg); } -.finding-details { display: grid; gap: 12px; padding: 0 14px 14px; }.finding-details > div { padding: 12px; border-radius: 6px; background: #080b0e; }.finding-details p { margin: 0; color: #d8dfe1; font-size: 12px; line-height: 1.6; }.detail-label { margin-bottom: 7px !important; color: #c5d0d2 !important; font: 10px var(--font-plex-mono), monospace !important; font-weight: 700; letter-spacing: .08em; }.fix-panel { border: 1px solid #16383b; background: #102327 !important; } +.bento-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.bento-cell { border-radius: var(--radius-card); padding: 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; min-height: 148px; text-align: center; } +.bento-headline { background: color-mix(in srgb, var(--teal) 13%, var(--paper)); border: 1px solid var(--line); } +.bento-headline strong { display: block; color: var(--ink); font-family: var(--font-playfair), serif; font-size: clamp(2.1rem, 4vw, 2.9rem); line-height: .95; letter-spacing: -0.01em; } +.bento-headline p { margin: 0; color: var(--muted); font-size: 12.5px; line-height: 1.4; } +.bento-amber { background: color-mix(in srgb, var(--amber) 16%, var(--paper)); border: 1px solid var(--line); } +.bento-amber svg { color: var(--amber); } +.bento-amber strong { display: block; font-size: 26px; color: var(--ink); letter-spacing: -0.02em; } +.bento-amber span { color: var(--muted); font-size: 12px; } +.bento-stat { border: 1px solid var(--line); background: var(--paper); } +.bento-stat strong { display: block; font-size: 22px; color: var(--ink); letter-spacing: -0.02em; } +.bento-stat span { color: var(--muted); font: 10px var(--font-plex-mono), monospace; text-transform: uppercase; } +.bento-list { border: 1px solid var(--line); } +.bento-list div { display: flex; gap: 8px; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; line-height: 1.4; text-align: left; } +.bento-list div + div { margin-top: 10px; } +.bento-list svg { flex: 0 0 auto; color: var(--teal); } +.bento-dark { background: var(--navy); color: var(--paper); } +.bento-dark svg { opacity: .8; } +.bento-dark strong { display: block; font-size: 28px; letter-spacing: -0.02em; } +.bento-dark span { opacity: .72; font-size: 12px; } -/* Animated readiness score ring (SVG-based, replaces old conic-gradient version) */ -.score-ring { - position: relative; - flex: 0 0 124px; - width: 124px; - height: 124px; +.hero-card-foot { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; } + +.reassurance { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 18px; padding: 70px 0 92px; color: var(--muted); } +.reassurance svg { color: var(--teal); opacity: .55; } +.reassurance blockquote { max-width: 640px; margin: 0; color: var(--ink); font-family: var(--font-playfair), serif; font-style: italic; font-weight: 500; font-size: clamp(1.5rem, 3vw, 2.1rem); line-height: 1.35; } +.reassurance figcaption { font: 11px var(--font-plex-mono), monospace; letter-spacing: .04em; text-transform: uppercase; } + +.how-it-works { padding: 100px 0; } +.section-heading { max-width: 620px; margin: 0 auto; text-align: center; } +.section-heading h2 { margin: 0; font-size: clamp(2.1rem, 4.5vw, 3.4rem); letter-spacing: -.01em; } +.flow-step-wrap { flex: 1; } +.flow-step { min-height: 236px; border: 1px solid var(--line); border-radius: var(--radius-card); padding: 28px; background: var(--paper); display: flex; flex-direction: column; } +.flow-step-wrap:last-child .flow-step { background: color-mix(in srgb, var(--amber) 14%, var(--paper)); } +.flow-icon { display: grid; place-items: center; width: 44px; height: 44px; color: var(--teal); border: 1px solid var(--line); border-radius: var(--radius-btn); margin-bottom: 26px; } +.flow-number { display: block; color: var(--muted); margin-bottom: 8px; font-size: 11px; } +.flow-step h3 { margin: 0 0 8px; font-family: var(--font-playfair), serif; font-size: 22px; font-weight: 600; } +.flow-step p { margin: 0; color: var(--muted); max-width: 220px; } +.flow-arrow { flex: 0 0 auto; color: var(--teal); } +.final-cta { display: flex; + flex-direction: column; align-items: center; - justify-content: center; - border-radius: 50%; - background: #0b0f12; + text-align: center; + gap: 22px; + margin: 12px auto 90px; + padding: 64px 48px; + border-radius: var(--radius-card); + background: color-mix(in srgb, var(--ink) 10%, var(--paper) 90%); + color: var(--ink); + position: relative; + overflow: hidden; + max-width: 1200px; +} +:root[data-theme='light'] .final-cta { background: var(--ink); color: var(--paper); } +.final-cta h2 { max-width: 620px; margin: 0; color: inherit; font-size: clamp(2.1rem, 4.5vw, 3.4rem); line-height: 1.02; letter-spacing: -.01em; } +.final-cta .mono-label { margin: 0; color: color-mix(in srgb, currentColor 55%, var(--teal)); } +.final-cta .button-invert { border-color: var(--teal); background: var(--teal); color: var(--paper); } +.final-cta .button-invert:hover { background: transparent; border-color: var(--teal); color: var(--teal); } +.cta-sparkle { position: absolute; top: 24px; right: 44px; opacity: .35; color: currentColor; pointer-events: none; } + +.flow-steps { display: flex; align-items: center; gap: 14px; margin-top: 52px; } .dropzone { min-height: 270px; border: 1px solid var(--line); border-radius: 18px; padding: 28px; cursor: pointer; transition: border-color 160ms ease, background 160ms ease, transform 160ms ease; } .dropzone:hover, .dropzone.is-dragging { border-color: var(--teal); background: color-mix(in srgb, var(--teal) 7%, var(--paper)); transform: translateY(-2px); } .upload-icon { display: grid; place-items: center; width: 48px; height: 48px; border: 1px solid var(--line); border-radius: 12px; color: var(--teal); } .dropzone-top { align-items: flex-start; } .dropzone h2 { margin-top: 28px; } .dropzone p { line-height: 1.7; } .dropzone p small { font-size: 12px; color: var(--muted); } .browse { color: var(--ink); font-weight: 600; } .file-name { display: inline-flex; align-items: center; gap: 7px; } .upload-note { display: flex; align-items: center; gap: 8px; color: var(--muted); font: 12px var(--font-plex-mono), monospace; margin-bottom: 24px; } .page-main:has(.results-head) { display: grid; grid-template-columns: minmax(280px, .7fr) minmax(0, 1.3fr); gap: 64px; align-items: start; } .results-head { display: block; align-items: center; padding-bottom: 0; border-bottom: 0; position: sticky; top: 24px; } .score-panel { min-width: 330px; } .score-circle { display: flex; align-items: baseline; justify-content: center; width: 190px; height: 190px; border: 10px solid var(--amber); border-radius: 50%; padding-top: 4px; } .score-circle strong { font-size: 68px; line-height: 1; letter-spacing: -.08em; } .score-circle span { color: var(--muted); font: 13px var(--font-plex-mono), monospace; margin-left: 3px; } .score-status { margin: 18px 0 20px; } .score-warning { display: flex; align-items: flex-start; gap: 9px; max-width: 320px; color: var(--muted); font-size: 13px; line-height: 1.45; } .score-warning svg { flex: 0 0 auto; color: var(--rust); margin-top: 2px; } .score-warning strong { color: var(--rust); } .report-meta { align-self: flex-end; text-align: right; color: var(--muted); font-size: 13px; } .report-meta p { margin: 0 0 8px; } .findings { min-width: 0; } .findings-heading { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 30px; } .findings h2 { margin: 0; } .findings h2 span { color: var(--muted); font-weight: 400; } .findings-note { margin: 0; color: var(--muted); font-size: 13px; } .finding { display: grid; grid-template-columns: 28px 1fr; gap: 14px; padding-left: 16px; } .finding-icon { color: var(--amber); padding-top: 2px; } .finding-critical .finding-icon { color: var(--rust); } .finding-minor .finding-icon { color: var(--teal); } .finding h3 { margin-bottom: 7px; } .center-page { min-height: auto; padding: 84px 24px 100px; } .processing { width: min(100%, 560px); } .processing h1 { margin-bottom: 38px; } .site-footer { margin-top: auto; } + +.report-page { background: #0b0f12; min-height: 100vh; } +.report-shell { padding: 38px 0 72px; } +.report-summary { display: flex; align-items: center; gap: 34px; padding: 4px 34px 34px; border-bottom: 1px solid #202831; } +.score-ring { + display: grid; + place-items: center; + flex: 0 0 124px; + width: 124px; + height: 124px; + position: relative; } .score-ring-svg { @@ -206,41 +231,57 @@ h1 { max-width: 760px; margin-bottom: 26px; font-size: clamp(3rem, 7vw, 6.5rem); .score-ring-track { fill: none; stroke: #202832; - stroke-width: 8; + stroke-width: 8px; } .score-ring-progress { fill: none; stroke: var(--amber); - stroke-width: 8; + stroke-width: 8px; stroke-linecap: round; } -.score-ring-inner { +.score-ring-inner { + display: grid; + place-items: center; + align-content: center; + width: 106px; + height: 106px; + border-radius: 50%; + background: #0b0f12; position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 106px; - height: 106px; - border-radius: 50%; - background: #0b0f12; + z-index: 1; } - +.score-ring strong { color: var(--amber); font-size: 32px; line-height: 1; letter-spacing: -.06em; } +.score-ring span { margin-top: 6px; color: #c1c8cb; font: 10px var(--font-plex-mono), monospace; letter-spacing: .04em; } +.score-copy .mono-label { margin-bottom: 4px; color: #d7dee0; font-size: 11px; font-weight: 700; letter-spacing: .08em; } +.score-copy h1 { margin: 0 0 8px; color: #f2f5f5; font-size: 27px; letter-spacing: -.04em; } +.score-copy > p:not(.mono-label) { max-width: 560px; margin: 0; color: #c1c8cb; font-size: 14px; } +.severity-counts { display: flex; gap: 18px; margin-top: 12px; font: 11px var(--font-plex-mono), monospace; } +.severity-counts span { display: inline-flex; align-items: center; gap: 6px; } +.severity-counts i { width: 7px; height: 7px; border-radius: 50%; } +.count-critical { color: #ff6c73; }.count-critical i { background: #ff6c73; }.count-warning { color: #f3ad15; }.count-warning i { background: #f3ad15; }.count-info { color: #22b9ef; }.count-info i { background: #22b9ef; } +.report-findings { padding: 16px 14px 0; } +.report-section-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }.report-section-heading .mono-label { margin: 0; color: #d7dee0; font-size: 11px; font-weight: 700; letter-spacing: .08em; }.report-id { margin: 0; color: #7f8b91; font-size: 10px; } +.report-finding-list { display: grid; gap: 8px; } +.report-finding { overflow: hidden; border: 1px solid #202832; border-radius: 8px; background: #101419; }.report-finding.is-open { border-color: #72551a; background: #171b21; } +.report-finding-trigger { display: grid; grid-template-columns: 72px minmax(0, 1fr) 18px; align-items: center; gap: 12px; width: 100%; padding: 13px 16px; border: 0; background: transparent; color: #eef2f1; text-align: left; cursor: pointer; }.report-finding-trigger:hover { background: #171b21; } +.severity-badge { display: inline-flex; align-items: center; gap: 5px; width: max-content; padding: 3px 7px; border: 1px solid currentColor; border-radius: 3px; font: 9px var(--font-plex-mono), monospace; font-weight: 700; letter-spacing: .05em; }.critical .severity-badge { color: #ff6068; background: #241519; }.warning .severity-badge { color: #f3ad15; background: #2a2416; }.info .severity-badge { color: #22b9ef; background: #13252d; } +.finding-title { display: grid; gap: 2px; min-width: 0; }.finding-title .mono { overflow: hidden; color: #d4dadd; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }.finding-title strong { color: #edf1f2; font-size: 13px; }.finding-chevron { color: #68747a; transition: transform 160ms ease; }.is-open .finding-chevron { transform: rotate(180deg); } +.finding-details { display: grid; gap: 12px; padding: 0 14px 14px; }.finding-details > div { padding: 12px; border-radius: 6px; background: #080b0e; }.finding-details p { margin: 0; color: #d8dfe1; font-size: 12px; line-height: 1.6; }.detail-label { margin-bottom: 7px !important; color: #c5d0d2 !important; font: 10px var(--font-plex-mono), monospace !important; font-weight: 700; letter-spacing: .08em; }.fix-panel { border: 1px solid #16383b; background: #102327 !important; } @media (max-width: 767px) { .shell { width: min(100% - 32px, 1120px); } - .landing-hero { grid-template-columns: 1fr; gap: 48px; padding: 66px 0 70px; } - .hero-content h1 { font-size: clamp(3.2rem, 15vw, 5rem); } + .landing-hero { grid-template-columns: 1fr; gap: 44px; padding: 60px 0 64px; } + .hero-content h1 { font-size: clamp(3rem, 15vw, 4.6rem); } .hero-copy { font-size: 18px; } - .hero-card { padding: 20px; } - .reassurance { grid-template-columns: 1fr; gap: 12px; } - .step-cards { grid-template-columns: 1fr; } - .step-card { min-height: auto; } - .flow-steps { grid-template-columns: 1fr; gap: 0; } - .flow-step-wrap { display: block; } + .bento-cell { padding: 16px; min-height: 118px; } + .bento-headline strong { font-size: 2rem; } + .reassurance { padding: 48px 0 56px; } + .flow-steps { flex-direction: column; gap: 14px; margin-top: 36px; } .flow-step { min-height: auto; } - .flow-arrow { display: block; margin: -1px auto; transform: rotate(90deg); } + .flow-arrow { transform: rotate(90deg); } + .final-cta { padding: 44px 26px; } + .final-cta .button-invert { width: 100%; justify-content: center; } .footer-brand, .footer-nav, .footer-legal { justify-content: flex-start; text-align: left; } .site-footer { grid-template-columns: 1fr; gap: 20px; } .page-main:has(.results-head) { display: block; } @@ -250,10 +291,6 @@ h1 { max-width: 760px; margin-bottom: 26px; font-size: clamp(3rem, 7vw, 6.5rem); .report-meta { margin-top: 28px; text-align: left; } .score-circle { width: 165px; height: 165px; } .score-circle strong { font-size: 58px; } - .step-card .step-number { margin-bottom: 30px; } - .final-cta { align-items: flex-start; flex-direction: column; padding: 58px 0 70px; } - .final-cta .button { width: 100%; justify-content: space-between; } - .site-header { padding: 20px 0; } .hero { padding: 74px 0 84px; } h1 { font-size: clamp(3.1rem, 15vw, 5rem); } @@ -266,18 +303,3 @@ h1 { max-width: 760px; margin-bottom: 26px; font-size: clamp(3rem, 7vw, 6.5rem); .upload-grid { gap: 16px; } .dropzone { min-height: 220px; } } - -.dropzone { min-width: 0; } -.file-row { min-width: 0; } -.file-name { - min-width: 0; - flex: 1 1 auto; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.file-name small { - flex: 0 0 auto; - white-space: nowrap; -} -.remove-file { flex: 0 0 auto; } diff --git a/apps/web/app/icon.png b/apps/web/app/icon.png new file mode 100644 index 0000000..9f7cdae Binary files /dev/null and b/apps/web/app/icon.png differ diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 860f55d..d67b3dd 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,14 +1,15 @@ import { Analytics } from '@vercel/analytics/next' -import { Geist, Geist_Mono } from 'next/font/google' +import { Geist_Mono, Montserrat, Playfair_Display } from 'next/font/google' import type { Metadata, Viewport } from 'next' import './globals.css' - -const interTight = Geist({ variable: '--font-inter-tight', subsets: ['latin'] }) +const montserrat = Montserrat({ variable: '--font-montserrat', subsets: ['latin'], weight: ['400', '500', '600', '700'] }) +const playfair = Playfair_Display({ variable: '--font-playfair', subsets: ['latin'], weight: ['500', '600', '700'], style: ['normal', 'italic'] }) const plexMono = Geist_Mono({ variable: '--font-plex-mono', subsets: ['latin'] }) export const metadata: Metadata = { - title: 'ClaimClear', + title: 'ClaimClear — Catch claim errors before submission', description: 'Audit clinical records and draft insurance claims against known rejection patterns before submission.', + generator: 'v0.app', } export const viewport: Viewport = { @@ -19,7 +20,7 @@ export const viewport: Viewport = { export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( -
+ {children} {process.env.NODE_ENV === 'production' &&CLAIMS, MADE CLEAR
+ClaimClear reviews your clinical record and draft claim in one calm, guided pass-so avoidable errors do not become delays.
+