diff --git a/src/medcheck/i18n/de.json b/src/medcheck/i18n/de.json index 0fc5384..b5f4656 100644 --- a/src/medcheck/i18n/de.json +++ b/src/medcheck/i18n/de.json @@ -21,5 +21,88 @@ "none_specified": "Keine angegeben.", "field_value": "Wert", "field_key": "Feld", - "disclaimer": "HAFTUNGSAUSSCHLUSS: Dieser Bericht wurde von einem automatisierten KI-System (MedCheck) erstellt und soll qualifizierte medizinische Fachkräfte unterstützen. Er stellt keine endgültige klinische Diagnose dar. Alle Befunde müssen von einem zugelassenen Radiologen oder Arzt überprüft und validiert werden." -} \ No newline at end of file + "disclaimer": "HAFTUNGSAUSSCHLUSS: Dieser Bericht wurde von einem automatisierten KI-System (MedCheck) erstellt und soll qualifizierte medizinische Fachkräfte unterstützen. Er stellt keine endgültige klinische Diagnose dar. Alle Befunde müssen von einem zugelassenen Radiologen oder Arzt überprüft und validiert werden.", + "ui_title": "MedCheck — KI-gestützte Analyse medizinischer Bildgebung", + "ui_tagline": "KI-gestützte Analyse medizinischer Bildgebung", + "ui_language": "Sprache", + "ui_hero_overline": "Analyse-Assistent", + "ui_hero_title": "Bildgebung in drei Schritten analysieren", + "ui_hero_lead": "Bilddaten laden, klinischen Kontext ergänzen und eine KI-gestützte Auswertung mit strukturiertem, radiologischem Bericht starten.", + "ui_preview_title": "Vorschau", + "ui_preview_body": "Die Analyse aus dem Browser ist noch nicht verfügbar — der Schritt „Analysieren“ liefert derzeit 501 Not Implemented.", + "ui_preview_cli": "Nutzen Sie stattdessen die Kommandozeile:", + "ui_step_label": "Schritt", + "ui_step1": "Daten laden", + "ui_step2": "Klinischer Kontext", + "ui_step3": "Analysieren", + "ui_upload_heading": "Bilddatei hochladen", + "ui_dropzone_cta": "Klicken zum Auswählen", + "ui_dropzone_or": "oder Datei hierher ziehen", + "ui_dropzone_formats": "DICOM, NIfTI, PNG, JPEG, TIFF — bis 500 MB", + "ui_autodetect": "Automatische Erkennung aktiv — das Dateiformat wird selbstständig identifiziert", + "ui_or": "ODER", + "ui_remote_url": "Externe URL", + "ui_url_hint": "Link zu einer DICOM-Datei oder einem easyRadiology-Portal", + "ui_patient_code": "Patienten-Code / ID", + "ui_back": "Zurück", + "ui_next_context": "Weiter: Klinischer Kontext", + "ui_next_analyze": "Weiter: Analysieren", + "ui_symptoms": "Symptome / Leitbeschwerde", + "ui_symptoms_hint": "z. B. anhaltende Kopfschmerzen, Sehstörungen, mediale Knieschmerzen", + "ui_trauma": "Trauma / Unfallhergang", + "ui_suspected_dx": "Verdachtsdiagnose", + "ui_anatomy": "Anatomie / Region", + "ui_anatomy_placeholder": "Region wählen", + "ui_anat_group_msk": "Muskuloskelettal", + "ui_anat_group_other": "Weitere Regionen", + "ui_anat_knee": "Knie", + "ui_anat_shoulder": "Schulter", + "ui_anat_hip": "Hüfte", + "ui_anat_wrist": "Handgelenk", + "ui_anat_ankle": "Sprunggelenk", + "ui_anat_spine": "Wirbelsäule", + "ui_anat_brain": "Gehirn", + "ui_anat_chest": "Thorax", + "ui_anat_abdomen": "Abdomen", + "ui_anat_other": "Sonstiges / Ganzkörper", + "ui_modules": "Analysemodule", + "ui_mod_ml": "ML-Klassifikation", + "ui_mod_seg": "Vision-Segmentierung", + "ui_mod_anomaly": "Anomalie-Erkennung", + "ui_mod_measure": "Automatische Messungen", + "ui_mod_compare": "Voraufnahmen-Vergleich", + "ui_mod_report": "Berichterstellung", + "ui_model": "KI-Modell", + "ui_model_claude": "Claude Opus 4.8 (beste Qualität)", + "ui_model_openai": "GPT-5.5 (beste Auflösung)", + "ui_model_gemini": "Gemini 3.5 Flash (am schnellsten)", + "ui_model_local": "Lokales LLaVA-Med (offline)", + "ui_report_language": "Berichtssprache", + "ui_report_format": "Berichtsformat", + "ui_fmt_pdf": "PDF (strukturierter Radiologiebericht)", + "ui_fmt_json": "JSON (strukturierte Daten)", + "ui_fmt_html": "HTML (interaktiv)", + "ui_consent_label": "Ich stimme zu, dass patientenbezogene Bilddaten zur Analyse an den ausgewählten externen Cloud-LLM-Anbieter übertragen werden.", + "ui_consent_hint": "Erforderlich für Cloud-Modelle. Ohne Zustimmung verlassen keine Daten Ihr System.", + "ui_consent_required": "Bitte stimmen Sie der Cloud-Übertragung zu oder wählen Sie das lokale Modell.", + "ui_start": "Analyse starten", + "ui_results": "Ergebnisse", + "ui_progress_waiting": "Warten auf den Start der Analyse …", + "ui_prog_sending": "Anfrage wird an den Server gesendet …", + "ui_no_results": "Noch keine Ergebnisse. Konfigurieren Sie die Schritte oben und starten Sie die Analyse.", + "ui_dl_pdf": "PDF-Bericht", + "ui_dl_json": "JSON-Daten", + "ui_dl_images": "Annotierte Bilder", + "ui_dl_pending": "Verfügbar nach Abschluss der Analyse", + "ui_footer_notice_title": "Kein Medizinprodukt", + "ui_footer_research": "Nur für Forschungs- und Ausbildungszwecke.", + "ui_footer_docs": "Dokumentation", + "ui_footer_project": "Projekt", + "ui_footer_license": "Lizenz (Apache 2.0)", + "ui_footer_security": "Sicherheitsrichtlinie", + "ui_footer_changelog": "Änderungsprotokoll", + "ui_footer_intended": "Verwendungszweck", + "ui_footer_modelcard": "Model Card", + "ui_footer_github": "GitHub-Repository", + "ui_version": "Version" +} diff --git a/src/medcheck/i18n/en.json b/src/medcheck/i18n/en.json index 39a57de..0c4230b 100644 --- a/src/medcheck/i18n/en.json +++ b/src/medcheck/i18n/en.json @@ -21,5 +21,88 @@ "none_specified": "None specified.", "field_value": "Value", "field_key": "Field", - "disclaimer": "DISCLAIMER: This report was generated by an automated AI system (MedCheck) and is intended to assist qualified medical professionals. It does not constitute a final clinical diagnosis. All findings must be reviewed and validated by a licensed radiologist or physician." -} \ No newline at end of file + "disclaimer": "DISCLAIMER: This report was generated by an automated AI system (MedCheck) and is intended to assist qualified medical professionals. It does not constitute a final clinical diagnosis. All findings must be reviewed and validated by a licensed radiologist or physician.", + "ui_title": "MedCheck — AI-assisted medical imaging analysis", + "ui_tagline": "AI-assisted medical imaging analysis", + "ui_language": "Language", + "ui_hero_overline": "Analysis wizard", + "ui_hero_title": "Analyze imaging data in three steps", + "ui_hero_lead": "Load imaging data, add clinical context, and run an AI-assisted analysis with a structured, radiology-style report.", + "ui_preview_title": "Preview", + "ui_preview_body": "Running an analysis from the browser is not yet available — the Analyze step currently returns 501 Not Implemented.", + "ui_preview_cli": "Use the command line instead:", + "ui_step_label": "Step", + "ui_step1": "Load data", + "ui_step2": "Clinical context", + "ui_step3": "Analyze", + "ui_upload_heading": "Upload imaging file", + "ui_dropzone_cta": "Click to browse", + "ui_dropzone_or": "or drag & drop a file here", + "ui_dropzone_formats": "DICOM, NIfTI, PNG, JPEG, TIFF — up to 500 MB", + "ui_autodetect": "Auto-detect active — the file format will be identified automatically", + "ui_or": "OR", + "ui_remote_url": "Remote URL", + "ui_url_hint": "Link to a DICOM file or an easyRadiology portal", + "ui_patient_code": "Patient code / ID", + "ui_back": "Back", + "ui_next_context": "Next: Clinical context", + "ui_next_analyze": "Next: Analyze", + "ui_symptoms": "Symptoms / chief complaint", + "ui_symptoms_hint": "e.g. persistent headache, vision changes, medial knee pain", + "ui_trauma": "Trauma / mechanism", + "ui_suspected_dx": "Suspected diagnosis", + "ui_anatomy": "Anatomy / region", + "ui_anatomy_placeholder": "Select region", + "ui_anat_group_msk": "Musculoskeletal", + "ui_anat_group_other": "Other regions", + "ui_anat_knee": "Knee", + "ui_anat_shoulder": "Shoulder", + "ui_anat_hip": "Hip", + "ui_anat_wrist": "Wrist", + "ui_anat_ankle": "Ankle", + "ui_anat_spine": "Spine", + "ui_anat_brain": "Brain", + "ui_anat_chest": "Chest / thorax", + "ui_anat_abdomen": "Abdomen", + "ui_anat_other": "Other / whole body", + "ui_modules": "Analysis modules", + "ui_mod_ml": "ML classification", + "ui_mod_seg": "Vision segmentation", + "ui_mod_anomaly": "Anomaly detection", + "ui_mod_measure": "Auto measurements", + "ui_mod_compare": "Prior comparison", + "ui_mod_report": "Report generation", + "ui_model": "AI model", + "ui_model_claude": "Claude Opus 4.8 (best quality)", + "ui_model_openai": "GPT-5.5 (best resolution)", + "ui_model_gemini": "Gemini 3.5 Flash (fastest)", + "ui_model_local": "Local LLaVA-Med (offline)", + "ui_report_language": "Report language", + "ui_report_format": "Report format", + "ui_fmt_pdf": "PDF (structured radiology report)", + "ui_fmt_json": "JSON (structured data)", + "ui_fmt_html": "HTML (interactive)", + "ui_consent_label": "I consent to transmitting patient-derived imaging data to the selected external cloud LLM provider for analysis.", + "ui_consent_hint": "Required for cloud models. Without consent, no data leaves your machine.", + "ui_consent_required": "Please consent to the cloud transmission or choose the local model.", + "ui_start": "Start analysis", + "ui_results": "Results", + "ui_progress_waiting": "Waiting for analysis to start …", + "ui_prog_sending": "Sending request to the server …", + "ui_no_results": "No results yet. Configure the steps above and start the analysis.", + "ui_dl_pdf": "PDF report", + "ui_dl_json": "JSON data", + "ui_dl_images": "Annotated images", + "ui_dl_pending": "Available after the analysis completes", + "ui_footer_notice_title": "Not a medical device", + "ui_footer_research": "For research and educational use only.", + "ui_footer_docs": "Documentation", + "ui_footer_project": "Project", + "ui_footer_license": "License (Apache 2.0)", + "ui_footer_security": "Security policy", + "ui_footer_changelog": "Changelog", + "ui_footer_intended": "Intended use", + "ui_footer_modelcard": "Model card", + "ui_footer_github": "GitHub repository", + "ui_version": "Version" +} diff --git a/src/medcheck/i18n/es.json b/src/medcheck/i18n/es.json index 006ed35..5f42688 100644 --- a/src/medcheck/i18n/es.json +++ b/src/medcheck/i18n/es.json @@ -21,5 +21,88 @@ "none_specified": "Ninguna especificada.", "field_value": "Valor", "field_key": "Campo", - "disclaimer": "AVISO LEGAL: Este informe fue generado por un sistema automatizado de IA (MedCheck) y está diseñado para asistir a profesionales médicos calificados. No constituye un diagnóstico clínico definitivo. Todos los hallazgos deben ser revisados y validados por un radiólogo o médico licenciado." -} \ No newline at end of file + "disclaimer": "AVISO LEGAL: Este informe fue generado por un sistema automatizado de IA (MedCheck) y está diseñado para asistir a profesionales médicos calificados. No constituye un diagnóstico clínico definitivo. Todos los hallazgos deben ser revisados y validados por un radiólogo o médico licenciado.", + "ui_title": "MedCheck — Análisis de imágenes médicas asistido por IA", + "ui_tagline": "Análisis de imágenes médicas asistido por IA", + "ui_language": "Idioma", + "ui_hero_overline": "Asistente de análisis", + "ui_hero_title": "Analizar imágenes en tres pasos", + "ui_hero_lead": "Cargue los datos de imagen, añada el contexto clínico e inicie un análisis asistido por IA con un informe radiológico estructurado.", + "ui_preview_title": "Vista previa", + "ui_preview_body": "El análisis desde el navegador aún no está disponible — el paso «Analizar» devuelve actualmente 501 Not Implemented.", + "ui_preview_cli": "Utilice la línea de comandos en su lugar:", + "ui_step_label": "Paso", + "ui_step1": "Cargar datos", + "ui_step2": "Contexto clínico", + "ui_step3": "Analizar", + "ui_upload_heading": "Subir archivo de imagen", + "ui_dropzone_cta": "Haga clic para explorar", + "ui_dropzone_or": "o arrastre y suelte un archivo aquí", + "ui_dropzone_formats": "DICOM, NIfTI, PNG, JPEG, TIFF — hasta 500 MB", + "ui_autodetect": "Detección automática activa — el formato del archivo se identificará automáticamente", + "ui_or": "O", + "ui_remote_url": "URL remota", + "ui_url_hint": "Enlace a un archivo DICOM o a un portal easyRadiology", + "ui_patient_code": "Código de paciente / ID", + "ui_back": "Atrás", + "ui_next_context": "Siguiente: Contexto clínico", + "ui_next_analyze": "Siguiente: Analizar", + "ui_symptoms": "Síntomas / motivo principal", + "ui_symptoms_hint": "p. ej. cefalea persistente, alteraciones visuales, dolor medial de rodilla", + "ui_trauma": "Trauma / mecanismo", + "ui_suspected_dx": "Diagnóstico sospechado", + "ui_anatomy": "Anatomía / región", + "ui_anatomy_placeholder": "Seleccionar región", + "ui_anat_group_msk": "Musculoesquelético", + "ui_anat_group_other": "Otras regiones", + "ui_anat_knee": "Rodilla", + "ui_anat_shoulder": "Hombro", + "ui_anat_hip": "Cadera", + "ui_anat_wrist": "Muñeca", + "ui_anat_ankle": "Tobillo", + "ui_anat_spine": "Columna vertebral", + "ui_anat_brain": "Cerebro", + "ui_anat_chest": "Tórax", + "ui_anat_abdomen": "Abdomen", + "ui_anat_other": "Otro / cuerpo entero", + "ui_modules": "Módulos de análisis", + "ui_mod_ml": "Clasificación ML", + "ui_mod_seg": "Segmentación de visión", + "ui_mod_anomaly": "Detección de anomalías", + "ui_mod_measure": "Mediciones automáticas", + "ui_mod_compare": "Comparación con estudios previos", + "ui_mod_report": "Generación de informes", + "ui_model": "Modelo de IA", + "ui_model_claude": "Claude Opus 4.8 (mejor calidad)", + "ui_model_openai": "GPT-5.5 (mejor resolución)", + "ui_model_gemini": "Gemini 3.5 Flash (el más rápido)", + "ui_model_local": "LLaVA-Med local (sin conexión)", + "ui_report_language": "Idioma del informe", + "ui_report_format": "Formato del informe", + "ui_fmt_pdf": "PDF (informe radiológico estructurado)", + "ui_fmt_json": "JSON (datos estructurados)", + "ui_fmt_html": "HTML (interactivo)", + "ui_consent_label": "Doy mi consentimiento para transmitir los datos de imagen del paciente al proveedor cloud de LLM seleccionado para su análisis.", + "ui_consent_hint": "Obligatorio para modelos en la nube. Sin consentimiento, ningún dato sale de su equipo.", + "ui_consent_required": "Por favor, consienta la transmisión a la nube o elija el modelo local.", + "ui_start": "Iniciar análisis", + "ui_results": "Resultados", + "ui_progress_waiting": "Esperando el inicio del análisis …", + "ui_prog_sending": "Enviando la solicitud al servidor …", + "ui_no_results": "Aún no hay resultados. Configure los pasos anteriores e inicie el análisis.", + "ui_dl_pdf": "Informe PDF", + "ui_dl_json": "Datos JSON", + "ui_dl_images": "Imágenes anotadas", + "ui_dl_pending": "Disponible al finalizar el análisis", + "ui_footer_notice_title": "No es un producto sanitario", + "ui_footer_research": "Solo para investigación y formación.", + "ui_footer_docs": "Documentación", + "ui_footer_project": "Proyecto", + "ui_footer_license": "Licencia (Apache 2.0)", + "ui_footer_security": "Política de seguridad", + "ui_footer_changelog": "Registro de cambios", + "ui_footer_intended": "Uso previsto", + "ui_footer_modelcard": "Ficha del modelo", + "ui_footer_github": "Repositorio de GitHub", + "ui_version": "Versión" +} diff --git a/src/medcheck/i18n/fr.json b/src/medcheck/i18n/fr.json index 31e4a8f..d89566d 100644 --- a/src/medcheck/i18n/fr.json +++ b/src/medcheck/i18n/fr.json @@ -21,5 +21,88 @@ "none_specified": "Aucune spécifiée.", "field_value": "Valeur", "field_key": "Champ", - "disclaimer": "AVERTISSEMENT : Ce rapport a été généré par un système d'IA automatisé (MedCheck) et est destiné à assister les professionnels de santé qualifiés. Il ne constitue pas un diagnostic clinique définitif. Toutes les observations doivent être examinées et validées par un radiologue ou un médecin agréé." -} \ No newline at end of file + "disclaimer": "AVERTISSEMENT : Ce rapport a été généré par un système d'IA automatisé (MedCheck) et est destiné à assister les professionnels de santé qualifiés. Il ne constitue pas un diagnostic clinique définitif. Toutes les observations doivent être examinées et validées par un radiologue ou un médecin agréé.", + "ui_title": "MedCheck — Analyse d'imagerie médicale assistée par IA", + "ui_tagline": "Analyse d'imagerie médicale assistée par IA", + "ui_language": "Langue", + "ui_hero_overline": "Assistant d'analyse", + "ui_hero_title": "Analyser l'imagerie en trois étapes", + "ui_hero_lead": "Chargez les données d'imagerie, ajoutez le contexte clinique et lancez une analyse assistée par IA avec un rapport radiologique structuré.", + "ui_preview_title": "Aperçu", + "ui_preview_body": "L'analyse depuis le navigateur n'est pas encore disponible — l'étape « Analyser » renvoie actuellement 501 Not Implemented.", + "ui_preview_cli": "Utilisez plutôt la ligne de commande :", + "ui_step_label": "Étape", + "ui_step1": "Charger les données", + "ui_step2": "Contexte clinique", + "ui_step3": "Analyser", + "ui_upload_heading": "Téléverser un fichier d'imagerie", + "ui_dropzone_cta": "Cliquer pour parcourir", + "ui_dropzone_or": "ou glisser-déposer un fichier ici", + "ui_dropzone_formats": "DICOM, NIfTI, PNG, JPEG, TIFF — jusqu'à 500 Mo", + "ui_autodetect": "Détection automatique active — le format du fichier sera identifié automatiquement", + "ui_or": "OU", + "ui_remote_url": "URL distante", + "ui_url_hint": "Lien vers un fichier DICOM ou un portail easyRadiology", + "ui_patient_code": "Code patient / ID", + "ui_back": "Retour", + "ui_next_context": "Suivant : Contexte clinique", + "ui_next_analyze": "Suivant : Analyser", + "ui_symptoms": "Symptômes / motif principal", + "ui_symptoms_hint": "p. ex. céphalées persistantes, troubles visuels, douleur médiale du genou", + "ui_trauma": "Traumatisme / mécanisme", + "ui_suspected_dx": "Diagnostic suspecté", + "ui_anatomy": "Anatomie / région", + "ui_anatomy_placeholder": "Choisir une région", + "ui_anat_group_msk": "Musculo-squelettique", + "ui_anat_group_other": "Autres régions", + "ui_anat_knee": "Genou", + "ui_anat_shoulder": "Épaule", + "ui_anat_hip": "Hanche", + "ui_anat_wrist": "Poignet", + "ui_anat_ankle": "Cheville", + "ui_anat_spine": "Colonne vertébrale", + "ui_anat_brain": "Cerveau", + "ui_anat_chest": "Thorax", + "ui_anat_abdomen": "Abdomen", + "ui_anat_other": "Autre / corps entier", + "ui_modules": "Modules d'analyse", + "ui_mod_ml": "Classification ML", + "ui_mod_seg": "Segmentation vision", + "ui_mod_anomaly": "Détection d'anomalies", + "ui_mod_measure": "Mesures automatiques", + "ui_mod_compare": "Comparaison antérieure", + "ui_mod_report": "Génération de rapport", + "ui_model": "Modèle d'IA", + "ui_model_claude": "Claude Opus 4.8 (meilleure qualité)", + "ui_model_openai": "GPT-5.5 (meilleure résolution)", + "ui_model_gemini": "Gemini 3.5 Flash (le plus rapide)", + "ui_model_local": "LLaVA-Med local (hors ligne)", + "ui_report_language": "Langue du rapport", + "ui_report_format": "Format du rapport", + "ui_fmt_pdf": "PDF (rapport radiologique structuré)", + "ui_fmt_json": "JSON (données structurées)", + "ui_fmt_html": "HTML (interactif)", + "ui_consent_label": "J'accepte la transmission des données d'imagerie du patient au fournisseur cloud de LLM sélectionné à des fins d'analyse.", + "ui_consent_hint": "Requis pour les modèles cloud. Sans consentement, aucune donnée ne quitte votre machine.", + "ui_consent_required": "Veuillez consentir à la transmission cloud ou choisir le modèle local.", + "ui_start": "Lancer l'analyse", + "ui_results": "Résultats", + "ui_progress_waiting": "En attente du démarrage de l'analyse …", + "ui_prog_sending": "Envoi de la requête au serveur …", + "ui_no_results": "Pas encore de résultats. Configurez les étapes ci-dessus et lancez l'analyse.", + "ui_dl_pdf": "Rapport PDF", + "ui_dl_json": "Données JSON", + "ui_dl_images": "Images annotées", + "ui_dl_pending": "Disponible une fois l'analyse terminée", + "ui_footer_notice_title": "Pas un dispositif médical", + "ui_footer_research": "Réservé à la recherche et à la formation.", + "ui_footer_docs": "Documentation", + "ui_footer_project": "Projet", + "ui_footer_license": "Licence (Apache 2.0)", + "ui_footer_security": "Politique de sécurité", + "ui_footer_changelog": "Journal des modifications", + "ui_footer_intended": "Usage prévu", + "ui_footer_modelcard": "Fiche du modèle", + "ui_footer_github": "Dépôt GitHub", + "ui_version": "Version" +} diff --git a/src/medcheck/web/app.py b/src/medcheck/web/app.py index 2f1b432..46364f1 100644 --- a/src/medcheck/web/app.py +++ b/src/medcheck/web/app.py @@ -21,12 +21,17 @@ from medcheck import __version__ from medcheck.core.config import Settings +from medcheck.i18n import get_strings TEMPLATES_DIR = Path(__file__).parent / "templates" STATIC_DIR = Path(__file__).parent / "static" _api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) +# UI locales offered by the web page's language switcher. Keep in sync with the +# AnalyzeRequest.language pattern and the i18n catalogs (medcheck/i18n/*.json). +UI_LANGUAGES = ("en", "de", "fr", "es") + class AnalyzeRequest(BaseModel): """Validated request body for POST /api/analyze.""" @@ -201,8 +206,22 @@ def health() -> dict[str, Any]: return {"status": "ok", "version": __version__} @app.get("/", response_class=HTMLResponse) - def index(request: Request) -> Any: - return templates.TemplateResponse(request, "index.html", {"version": __version__}) + def index(request: Request, lang: str | None = None) -> Any: + # ?lang= switches the UI locale; anything unknown falls back to the + # configured default (and ultimately to English inside get_strings). + ui_lang = lang if lang in UI_LANGUAGES else settings.default_language + if ui_lang not in UI_LANGUAGES: + ui_lang = "en" + return templates.TemplateResponse( + request, + "index.html", + { + "version": __version__, + "t": get_strings(ui_lang), + "lang": ui_lang, + "languages": UI_LANGUAGES, + }, + ) @app.post("/api/analyze") def analyze( diff --git a/src/medcheck/web/static/app.js b/src/medcheck/web/static/app.js index dcd21ee..7e787aa 100644 --- a/src/medcheck/web/static/app.js +++ b/src/medcheck/web/static/app.js @@ -3,17 +3,26 @@ (function () { 'use strict'; - // Tab switching - function showTab(n) { + var CLOUD_MODELS = ['claude', 'openai', 'gemini']; + + // Wizard step switching (stepper buttons + prev/next buttons use data-goto) + function showStep(n) { document.querySelectorAll('.tab-pane').forEach(function (p) { p.classList.remove('active'); }); - document.querySelectorAll('.tab-btn').forEach(function (b) { - b.classList.remove('active'); + var pane = document.getElementById('pane-' + n); + if (pane) pane.classList.add('active'); + + document.querySelectorAll('.pdp-stepper-step').forEach(function (s) { + var step = parseInt(s.getAttribute('data-step'), 10); + s.classList.toggle('active', step === n); + s.classList.toggle('completed', step < n); + if (step === n) { + s.setAttribute('aria-current', 'step'); + } else { + s.removeAttribute('aria-current'); + } }); - document.getElementById('pane-' + n).classList.add('active'); - var tabs = document.querySelectorAll('.tab-btn'); - if (tabs[n - 1]) tabs[n - 1].classList.add('active'); } // File dropzone - use textContent for safe DOM updates @@ -23,35 +32,124 @@ var subtext = document.getElementById('dropzoneSubtext'); var sizeMB = (f.size / 1024 / 1024).toFixed(2); subtext.textContent = f.name + ' (' + sizeMB + ' MB)'; - document.getElementById('autoDetectBadge').style.display = 'inline-flex'; + var badge = document.getElementById('autoDetectBadge'); + if (badge) badge.classList.add('is-visible'); } } - function simulateProgress() { + function isCloudModel() { + var select = document.getElementById('modelSelect'); + return !!select && CLOUD_MODELS.indexOf(select.value) !== -1; + } + + function syncConsentVisibility() { + var block = document.getElementById('consentBlock'); + if (block) block.classList.toggle('is-visible', isCloudModel()); + } + + // Replace the results area content with a kit alert (text set via textContent, + // never innerHTML — the server response echoes user input). + function showResultAlert(kind, text) { + var results = document.getElementById('resultsContent'); + if (!results) return; + results.textContent = ''; + results.classList.remove('results-empty'); + var alert = document.createElement('div'); + alert.className = 'alert alert-' + kind; + alert.setAttribute('role', 'alert'); + var body = document.createElement('p'); + body.textContent = text; + alert.appendChild(body); + results.appendChild(alert); + alert.style.marginBottom = '1.5rem'; + } + + function setProgress(percent, labelText) { var fill = document.getElementById('progressFill'); var label = document.getElementById('progressLabel'); - var steps = [ - [10, 'Uploading data...'], - [30, 'Preprocessing imaging data...'], - [55, 'Running ML modules...'], - [75, 'Querying AI model...'], - [90, 'Generating report...'], - [100, 'Done!'] - ]; - var i = 0; - var tick = setInterval(function () { - if (i >= steps.length) { clearInterval(tick); return; } - fill.style.width = steps[i][0] + '%'; - label.textContent = steps[i][1]; - i++; - }, 800); + if (fill) fill.style.width = percent + '%'; + if (label && labelText) label.textContent = labelText; + } + + function submitAnalysis(form) { + var startBtn = document.getElementById('startBtn'); + var consent = document.getElementById('consentCheck'); + var cloud = isCloudModel(); + + if (cloud && (!consent || !consent.checked)) { + showResultAlert('danger', form.getAttribute('data-msg-consent')); + showStep(3); + if (consent) consent.focus(); + return; + } + + var urlInput = document.getElementById('sourceUrl'); + var fileInput = document.getElementById('fileInput'); + var source = (urlInput && urlInput.value.trim()) || + (fileInput && fileInput.files && fileInput.files[0] && fileInput.files[0].name) || + 'browser-upload'; + + var anatomy = document.getElementById('anatomy'); + var language = document.getElementById('reportLanguage'); + var format = document.getElementById('reportFormat'); + + var body = { + source: source, + report_format: format ? format.value : 'json', + language: language ? language.value : 'en', + allow_cloud_llm: !!(cloud && consent && consent.checked) + }; + if (anatomy && anatomy.value) body.anatomy = anatomy.value; + + if (startBtn) startBtn.disabled = true; + setProgress(30, form.getAttribute('data-msg-sending')); + + fetch(form.getAttribute('action'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }) + .then(function (resp) { + return resp.json().then(function (data) { + return { ok: resp.ok, status: resp.status, data: data }; + }); + }) + .then(function (result) { + var detail = result.data && result.data.detail; + if (typeof detail !== 'string') detail = JSON.stringify(result.data); + if (result.ok) { + setProgress(100, ''); + showResultAlert('success', detail); + } else { + setProgress(0, form.getAttribute('data-msg-waiting')); + // 501 = known preview limitation -> warning; anything else -> danger. + showResultAlert(result.status === 501 ? 'warning' : 'danger', detail); + } + }) + .catch(function (err) { + setProgress(0, form.getAttribute('data-msg-waiting')); + showResultAlert('danger', String(err)); + }) + .then(function () { + if (startBtn) startBtn.disabled = false; + }); } document.addEventListener('DOMContentLoaded', function () { + // Sticky header — must NOT be sticky at page load; scroll adds the class. + var siteHeader = document.getElementById('site-header'); + if (siteHeader) { + var onScroll = function () { + siteHeader.classList.toggle('is-sticky', window.scrollY > 0); + }; + window.addEventListener('scroll', onScroll, { passive: true }); + onScroll(); + } + // Any element with data-goto="N" switches to wizard step N. document.querySelectorAll('[data-goto]').forEach(function (el) { el.addEventListener('click', function () { - showTab(parseInt(el.getAttribute('data-goto'), 10)); + showStep(parseInt(el.getAttribute('data-goto'), 10)); }); }); @@ -59,6 +157,12 @@ var fileInput = document.getElementById('fileInput'); if (dropzone && fileInput) { dropzone.addEventListener('click', function () { fileInput.click(); }); + dropzone.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + fileInput.click(); + } + }); dropzone.addEventListener('dragover', function (e) { e.preventDefault(); dropzone.classList.add('dragover'); @@ -78,9 +182,18 @@ fileInput.addEventListener('change', function () { fileSelected(fileInput); }); } + var modelSelect = document.getElementById('modelSelect'); + if (modelSelect) { + modelSelect.addEventListener('change', syncConsentVisibility); + syncConsentVisibility(); + } + var form = document.getElementById('analyzeForm'); if (form) { - form.addEventListener('submit', function () { simulateProgress(); }); + form.addEventListener('submit', function (e) { + e.preventDefault(); + submitAnalysis(form); + }); } }); })(); diff --git a/src/medcheck/web/static/favicon.svg b/src/medcheck/web/static/favicon.svg new file mode 100644 index 0000000..2e66084 --- /dev/null +++ b/src/medcheck/web/static/favicon.svg @@ -0,0 +1 @@ + diff --git a/src/medcheck/web/templates/index.html b/src/medcheck/web/templates/index.html index 6252501..59d66b6 100644 --- a/src/medcheck/web/templates/index.html +++ b/src/medcheck/web/templates/index.html @@ -1,669 +1,1353 @@ - + - MedCheck – AI Medical Imaging Analysis + {{ t.ui_title }} + + - -
-
MedCheck
- v{{ version }} -
- AI Medical Imaging Analysis + + -
-

Upload imaging data and run AI-assisted clinical analysis in three steps.

- - -
- Preview: running an analysis from the browser is not yet available — - the Analyze step currently returns 501 Not Implemented - (#157). - Use the CLI instead: medcheck analyze /path/to/scans. -
- -
- - - -
+ +
+
+
+

{{ t.ui_hero_overline }}

+

{{ t.ui_hero_title }}

+

{{ t.ui_hero_lead }}

+
+
+
-
-
- - -
-
- -
-
🧻
-

Click to browse or drag & drop

-

DICOM, NIfTI, PNG, JPEG, TIFF — up to 500 MB

- -
-
-
-
-
-
-

Waiting for analysis to start…

+ +