diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 44decbe..0c20fb8 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -127,6 +127,8 @@ "name": "Name", "description": "Beschreibung", "warning": "Warnung", + "specNotApplyToVersion": "Spezifikation gilt nicht für diese IFC-Version", + "failureReason": "Fehlergrund", "globalId": "Globale ID", "tag": "Kennzeichnung", "reportBy": "Bericht von", @@ -156,25 +158,56 @@ "elementsPassedPrefix": "Elemente erfüllt", "applicability": "Anwendbarkeit", "all": "Alle", - "data": "Daten" + "data": "Daten", + "allEntityData": "Alle {{entity}} Daten" }, "phrases": { "moreOfSameType": "... {{count}} weitere vom gleichen Elementtyp ({{type}} mit Kennzeichnung {{tag}} und Globaler ID {{id}}) nicht angezeigt ...", "moreElementsNotShown": "... {{count}} weitere {{type}} Elemente nicht angezeigt von insgesamt {{total}} ..." }, "ids": { - "section": { - "loadBearing": "Tragende Struktur" + "sections": { + "projectDesignation": "Projektbezeichnung", + "projectPerimeter": "Projektperimeter", + "buildingDesignation": "Gebäudebezeichnung", + "storeyDesignation": "Geschossbezeichnung" }, - "description": { - "shouldHaveLoadBearing": "Alle Structure Elements sollte haben eine tragende Struktur" + "entities": { + "IfcProject": "Projekt", + "IfcSite": "Grundstück", + "IfcBuilding": "Gebäude", + "IfcBuildingStorey": "Geschoss" }, - "pattern": { - "nameShallBe": "Der Name muss {{value}} sein", + "fields": { + "name": "Name", + "description": "Beschreibung", + "phase": "Phase" + }, + "propertySets": { + "Pset_PropertyAgreement": "Eigenschaftsvereinbarung", + "Cust_Site": "Standort-Eigenschaften" + }, + "patterns": { + "nameShallBe": "{{field}} muss {{value}} sein", "descriptionRequired": "Die Beschreibung muss angegeben werden", "propertyRequired": "{{property}} muss im Eigenschaftssatz {{propertySet}} angegeben werden", "propertyValue": "{{property}} muss {{value}} im Eigenschaftssatz {{propertySet}} sein", - "enumRequired": "Eine der Aufzählungswerte {{values}} muss im Eigenschaftssatz {{propertySet}} angegeben werden" + "dataRequired": "{{property}} Daten müssen {{value}} sein und im Eigenschaftssatz {{propertySet}}", + "enumRequired": "{{field}} muss einer der folgenden Werte sein: {{values}}", + "propertyInSet": "Eigenschaft {{property}} im Satz {{propertySet}}", + "propertyWithValue": "Eigenschaft {{property}} im Satz {{propertySet}} mit Wert {{value}}" + }, + "enumerations": { + "51_0": "51.0", + "52_0": "52.0", + "53_0": "53.0", + "untergeschoss": "Untergeschoss", + "erdgeschoss": "Erdgeschoss", + "obergeschoss": "Obergeschoss", + "zwischengeschoss": "Zwischengeschoss", + "dach": "Dach", + "u01": "U01", + "da": "DA" } } }, @@ -182,6 +215,7 @@ "loading": { "pyodide": "Pyodide wird geladen...", "pyodideSuccess": "Pyodide erfolgreich geladen", + "idsFile": "IDS-Datei wird gelesen...", "translations": "Übersetzungen werden geladen...", "packages": "Erforderliche Pakete werden installiert...", "micropipPatch": "Micropip wird für Kompatibilitätsprüfungen angepasst...", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 23a670c..b65fbe2 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -135,6 +135,8 @@ "name": "Name", "description": "Description", "warning": "Warning", + "specNotApplyToVersion": "specification does not apply to this IFC version", + "failureReason": "Failure Reason", "globalId": "GlobalId", "tag": "Tag", "reportBy": "Report by", @@ -190,6 +192,7 @@ "loading": { "pyodide": "Loading Pyodide...", "pyodideSuccess": "Pyodide loaded successfully", + "idsFile": "Reading IDS file content...", "translations": "Loading translations...", "packages": "Installing required packages...", "micropipPatch": "Patching micropip for compatibility check bypass...", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 80a3389..49d7a8c 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -159,6 +159,11 @@ "all": "Tous", "data": "Données" }, + "specificationsPassedPrefix": "Spécifications réussies", + "requirementsPassedPrefix": "Exigences respectées", + "applicability": "Applicabilité", + "specNotApplyToVersion": "la spécification ne s'applique pas à cette version IFC", + "failureReason": "Raison de l'échec", "phrases": { "moreOfSameType": "... {{count}} autres du même type d'élément ({{type}} avec l'étiquette {{tag}} et l'ID global {{id}}) non affichés ...", "moreElementsNotShown": "... {{count}} autres éléments {{type}} non affichés sur un total de {{total}} ..." diff --git a/public/locales/it/translation.json b/public/locales/it/translation.json index 18bf448..fc3f71f 100644 --- a/public/locales/it/translation.json +++ b/public/locales/it/translation.json @@ -159,6 +159,11 @@ "all": "Tutti", "data": "Dati" }, + "specificationsPassedPrefix": "Specifiche superate", + "requirementsPassedPrefix": "Requisiti soddisfatti", + "applicability": "Applicabilità", + "specNotApplyToVersion": "la specifica non si applica a questa versione IFC", + "failureReason": "Motivo del fallimento", "phrases": { "moreOfSameType": "... {{count}} altri dello stesso tipo di elemento ({{type}} con Tag {{tag}} e ID Globale {{id}}) non mostrati ...", "moreElementsNotShown": "... {{count}} altri elementi {{type}} non mostrati su un totale di {{total}} ..." diff --git a/public/locales/rm/translation.json b/public/locales/rm/translation.json index 14692fe..44e336f 100644 --- a/public/locales/rm/translation.json +++ b/public/locales/rm/translation.json @@ -159,6 +159,11 @@ "all": "Tut", "data": "Datas" }, + "specificationsPassedPrefix": "Specificaziuns reussidas", + "requirementsPassedPrefix": "Pretensiuns satisfatgas", + "applicability": "Applicabilitad", + "specNotApplyToVersion": "la specificaziun na s'applitgescha betg a questa versiun IFC", + "failureReason": "Motiv dal falliment", "phrases": { "moreOfSameType": "... {{count}} dapli dal medem tip d'element ({{type}} cun Tag {{tag}} ed ID global {{id}}) betg mussads ...", "moreElementsNotShown": "... {{count}} ulteriurs elements {{type}} betg mussads ord in total da {{total}} ..." diff --git a/public/pyodideWorkerClean.js b/public/pyodideWorkerClean.js new file mode 100644 index 0000000..46e1157 --- /dev/null +++ b/public/pyodideWorkerClean.js @@ -0,0 +1,254 @@ +/* global importScripts */ +importScripts('https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js') + +let pyodide = null + +// Simple console message function +function getConsoleMessage(key, defaultMessage) { + return defaultMessage +} + +// Load Pyodide once +async function initializePyodide() { + if (pyodide !== null) { + return pyodide + } + + try { + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.pyodide', 'Loading Pyodide...'), + }) + + pyodide = await self.loadPyodide({ + indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/', + }) + + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.pyodideSuccess', 'Pyodide loaded successfully'), + }) + + return pyodide + } catch (error) { + self.postMessage({ + type: 'error', + message: getConsoleMessage('console.error.pyodideLoad', `Failed to load Pyodide: ${error.message}`), + }) + throw error + } +} + +const WORKER_VERSION = '3.0.0-clean' +console.log('pyodideWorkerClean version:', WORKER_VERSION) + +self.onmessage = async function (e) { + const { arrayBuffer, idsContent, fileName, language, generateBcf, idsFilename } = e.data + + try { + // Load Pyodide + await initializePyodide() + + // Install required packages + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.packages', 'Installing required packages...'), + }) + + await pyodide.loadPackage(['micropip', 'python-dateutil', 'six', 'numpy', 'setuptools']) + + // Bypass the Emscripten version compatibility check for wheels + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.micropipPatch', 'Patching micropip for compatibility...'), + }) + + await pyodide.runPythonAsync(` +import micropip +from micropip._micropip import WheelInfo +WheelInfo.check_compatible = lambda self: None + `) + + // Install IfcOpenShell and IfcTester + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.ifcOpenShell', 'Installing IfcOpenShell...'), + }) + + await pyodide.runPythonAsync(` +import micropip +await micropip.install('https://cdn.jsdelivr.net/gh/IfcOpenShell/wasm-wheels@33b437e5fd5425e606f34aff602c42034ff5e6dc/ifcopenshell-0.8.1+latest-cp312-cp312-emscripten_3_1_58_wasm32.whl') + `) + + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.dependencies', 'Installing additional dependencies...'), + }) + + await pyodide.runPythonAsync(` +import micropip +await micropip.install(['lark', 'ifctester==0.8.1', 'bcf-client==0.8.1', 'pystache'], keep_going=True) + `) + + // Write files to Pyodide filesystem + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.inputFiles', 'Processing input files...'), + }) + + pyodide.FS.writeFile('/input.ifc', new Uint8Array(arrayBuffer)) + if (idsContent) { + pyodide.FS.writeFile('/input.ids', idsContent) + } + + // Run validation using native IfcTester + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.validation', 'Running IFC validation...'), + }) + + const validationResult = await pyodide.runPythonAsync(` +import json +import os +import ifcopenshell +from ifctester import ids, reporter +from datetime import datetime + +# Open IFC file +print("Opening IFC file: /input.ifc") +ifc = ifcopenshell.open('/input.ifc') + +# Initialize basic results structure +results = { + "title": "IDS Validation Report", + "filename": "${fileName}", + "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "language_code": "${language}", + "status": True, + "validation_status": "success" +} + +# Check if IDS file exists and validate +if os.path.exists('/input.ids'): + print("Loading IDS specification: /input.ids") + ids_spec = ids.open('/input.ids') + + if ids_spec and ids_spec.specifications: + print("Validating IFC against IDS...") + ids_spec.validate(ifc) + + # Use IfcTester's native JSON reporter and transform to our frontend structure + try: + json_reporter = reporter.Json(ids_spec) + native_results = json_reporter.report() + + # Transform IfcTester's structure to match our frontend expectations + results.update(native_results) + results["filename"] = "${fileName}" + results["language_code"] = "${language}" + results["validation_status"] = "success" if results.get("status", False) else "failed" + + # Transform specifications to add missing fields our frontend expects + for spec in results.get("specifications", []): + # Add fields that our frontend expects but IfcTester doesn't provide + if "total_applicable" not in spec: + spec["total_applicable"] = 0 + if "total_applicable_pass" not in spec: + spec["total_applicable_pass"] = 0 + if "total_applicable_fail" not in spec: + spec["total_applicable_fail"] = 0 + + # Transform requirements to add missing fields + for req in spec.get("requirements", []): + # Map IfcTester's structure to our frontend structure + if "total_checks" not in req: + req["total_checks"] = req.get("total_applicable", 0) + if "cardinality" not in req: + req["cardinality"] = "required" + if "has_omitted_passes" not in req: + req["has_omitted_passes"] = False + if "has_omitted_failures" not in req: + req["has_omitted_failures"] = False + + # Transform entity data to match our frontend expectations + for entity_list in ["passed_entities", "failed_entities"]: + if entity_list in req: + transformed_entities = [] + for entity in req[entity_list]: + transformed_entity = { + "class": entity.get("class", "Unknown"), + "predefined_type": entity.get("predefined_type") or "None", + "name": entity.get("name") or "None", + "description": entity.get("description") or "None", + "global_id": entity.get("global_id") or "None", + "tag": entity.get("tag") or "None", + "reason": entity.get("reason", "") + } + transformed_entities.append(transformed_entity) + req[entity_list] = transformed_entities + + print("Successfully transformed IfcTester JSON results to frontend format") + + except Exception as e: + print(f"IfcTester JSON reporter error: {e}") + results["specifications"] = [] + results["status"] = False + results["validation_status"] = "failed" +else: + print("No IDS file provided") + results["specifications"] = [] + +# Return JSON string - let frontend handle all translations and enhancements +json.dumps(results, default=str, ensure_ascii=False) + `) + + // Parse the JSON result + const results = JSON.parse(validationResult) + + // Check for errors + if (results.error) { + throw new Error(results.error) + } + + // Add metadata for frontend + results.ui_language = language + results.available_languages = ['en', 'de', 'fr', 'it', 'rm'] + results.filename = fileName + results.validation_status = results.status ? 'success' : 'failed' + + // Attach idsFilename for tab title building (if provided) + if (idsFilename) { + results.ids_filename = idsFilename + } + + // Send results back to frontend + self.postMessage({ + type: 'complete', + results: results, + message: getConsoleMessage('console.success.processingComplete', 'Your files have been processed.'), + }) + + } catch (error) { + console.error('Worker error:', error) + + // Check for out of memory error + const errorStr = error.toString().toLowerCase() + if (errorStr.includes('out of memory') || errorStr.includes('internalerror: out of memory')) { + self.postMessage({ + type: 'error', + errorType: 'out_of_memory', + message: getConsoleMessage( + 'console.error.outOfMemory', + 'Pyodide ran out of memory. The page will reload automatically to free resources.', + ), + stack: error.stack, + }) + } else { + self.postMessage({ + type: 'error', + message: getConsoleMessage('console.error.generic', `An error occurred: ${error.message}`), + stack: error.stack, + }) + } + } +} diff --git a/public/python/model_checker.py b/public/python/model_checker.py index 96ea454..8ed96e7 100644 --- a/public/python/model_checker.py +++ b/public/python/model_checker.py @@ -6,48 +6,9 @@ import os import re import argparse - -# Dictionary of translations (we'll keep a small set here for demo purposes) -TRANSLATIONS = { - "en": { - "summary": "Summary", - "specifications": "Specifications", - "requirements": "Requirements", - "details": "Details", - "class": "Class", - "predefinedType": "PredefinedType", - "name": "Name", - "description": "Description", - "warning": "Warning", - "globalId": "GlobalId", - "tag": "Tag", - "status": { - "pass": "PASS", - "fail": "FAIL", - "untested": "UNTESTED", - "skipped": "SKIPPED" - } - }, - "de": { - "summary": "Zusammenfassung", - "specifications": "Spezifikationen", - "requirements": "Anforderungen", - "details": "Details", - "class": "Klasse", - "predefinedType": "Vordefinierter Typ", - "name": "Name", - "description": "Beschreibung", - "warning": "Warnung", - "globalId": "Globale ID", - "tag": "Kennzeichnung", - "status": { - "pass": "BESTANDEN", - "fail": "FEHLGESCHLAGEN", - "untested": "UNGETESTET", - "skipped": "ÜBERSPRUNGEN" - } - } -} +import json +import base64 +from datetime import datetime def check_dependencies(): """Check and install required dependencies.""" @@ -70,83 +31,137 @@ def check_dependencies(): import ifcopenshell from ifctester import ids, reporter -def translate_html_report(html_content, lang): - """ - Translate key phrases in the HTML report based on the language. +def extract_requirement_data(requirement): + """Extract structured data from IDS requirement for JSON output""" + req_data = { + "type": requirement.__class__.__name__, + "cardinality": getattr(requirement, 'cardinality', 'required'), + "status": "pass" if not getattr(requirement, 'failed_entities', []) else "fail" + } - Args: - html_content: HTML content to translate - lang: Language code (e.g., 'en', 'de') + # Extract requirement details based on type + if hasattr(requirement, 'name') and requirement.name: + req_data["name"] = str(requirement.name) + if hasattr(requirement, 'value') and requirement.value: + req_data["value"] = str(requirement.value) + if hasattr(requirement, 'propertySet') and requirement.propertySet: + req_data["propertySet"] = str(requirement.propertySet) + if hasattr(requirement, 'baseName') and requirement.baseName: + req_data["baseName"] = str(requirement.baseName) + if hasattr(requirement, 'system') and requirement.system: + req_data["system"] = str(requirement.system) + if hasattr(requirement, 'instructions') and requirement.instructions: + req_data["instructions"] = str(requirement.instructions) - Returns: - Translated HTML content - """ - if lang == "en" or lang not in TRANSLATIONS: - print(f"No translation needed for language: {lang}") - return html_content - - print(f"Translating HTML report to {lang}") - translations = TRANSLATIONS[lang] - - # Simple translation of key phrases - translatable_terms = [ - ("Summary", "summary"), - ("Specifications", "specifications"), - ("Requirements", "requirements"), - ("Details", "details"), - ("Class", "class"), - ("PredefinedType", "predefinedType"), - ("Name", "name"), - ("Description", "description"), - ("Warning", "warning"), - ("GlobalId", "globalId"), - ("Tag", "tag"), - ("PASS", "status.pass"), - ("FAIL", "status.fail"), - ("UNTESTED", "status.untested"), - ("SKIPPED", "status.skipped") - ] - - # Apply translations - for english_term, field_path in translatable_terms: - # Handle nested fields like status.pass - field_parts = field_path.split('.') - if len(field_parts) > 1: - translation = translations[field_parts[0]][field_parts[1]] - else: - translation = translations[field_path] + # Handle restrictions and patterns + if hasattr(requirement, 'restriction'): + restriction = requirement.restriction + if hasattr(restriction, 'enumeration') and restriction.enumeration: + req_data["enumeration"] = [str(val) for val in restriction.enumeration] + if hasattr(restriction, 'pattern') and restriction.pattern: + req_data["pattern"] = str(restriction.pattern) + if hasattr(restriction, 'bounds') and restriction.bounds: + req_data["bounds"] = str(restriction.bounds) + + # Extract failed/passed entities for detailed reporting + if hasattr(requirement, 'failed_entities'): + req_data["failed_entities"] = [] + for entity in requirement.failed_entities[:10]: # Limit to first 10 + entity_data = { + "global_id": getattr(entity, 'GlobalId', ''), + "class": entity.__class__.__name__, + "predefined_type": getattr(entity, 'PredefinedType', 'NOTDEFINED'), + "name": getattr(entity, 'Name', ''), + "tag": getattr(entity, 'Tag', ''), + "description": getattr(entity, 'Description', ''), + "reason": '' # TODO: Add failure reason if available + } + req_data["failed_entities"].append(entity_data) + + if hasattr(requirement, 'passed_entities'): + req_data["passed_entities"] = [] + for entity in requirement.passed_entities[:10]: # Limit to first 10 + entity_data = { + "global_id": getattr(entity, 'GlobalId', ''), + "class": entity.__class__.__name__, + "predefined_type": getattr(entity, 'PredefinedType', 'NOTDEFINED'), + "name": getattr(entity, 'Name', ''), + "tag": getattr(entity, 'Tag', ''), + "description": getattr(entity, 'Description', ''), + "reason": '' + } + req_data["passed_entities"].append(entity_data) + + return req_data + +def extract_applicability_data(applicability): + """Extract applicability data for JSON output""" + if not applicability: + return None - # Use regex with word boundaries to avoid partial replacements - html_content = re.sub(r'\b' + english_term + r'\b', translation, html_content) + app_data = {} - print(f"HTML report translation to {lang} completed") - return html_content + if hasattr(applicability, 'entity') and applicability.entity: + app_data["entity"] = { + "name": str(applicability.entity.name) if applicability.entity.name else None, + "predefinedType": str(applicability.entity.predefinedType) if hasattr(applicability.entity, 'predefinedType') and applicability.entity.predefinedType else None + } + + if hasattr(applicability, 'classification') and applicability.classification: + app_data["classification"] = [] + classifications = applicability.classification if isinstance(applicability.classification, list) else [applicability.classification] + for cls in classifications: + cls_data = { + "system": str(cls.system) if cls.system else None, + "value": str(cls.value) if cls.value else None + } + app_data["classification"].append(cls_data) + + if hasattr(applicability, 'attribute') and applicability.attribute: + app_data["attribute"] = [] + attributes = applicability.attribute if isinstance(applicability.attribute, list) else [applicability.attribute] + for attr in attributes: + attr_data = { + "name": str(attr.name) if attr.name else None, + "value": str(attr.value) if attr.value else None + } + app_data["attribute"].append(attr_data) + + if hasattr(applicability, 'property') and applicability.property: + app_data["property"] = [] + properties = applicability.property if isinstance(applicability.property, list) else [applicability.property] + for prop in properties: + prop_data = { + "propertySet": str(prop.propertySet) if prop.propertySet else None, + "baseName": str(prop.baseName) if prop.baseName else None, + "value": str(prop.value) if prop.value else None + } + app_data["property"].append(prop_data) + + if hasattr(applicability, 'material') and applicability.material: + app_data["material"] = [] + materials = applicability.material if isinstance(applicability.material, list) else [applicability.material] + for mat in materials: + mat_data = { + "value": str(mat.value) if mat.value else None + } + app_data["material"].append(mat_data) + + return app_data if app_data else None -def test_ifc(ifc_path: str, ids_path: str, report_path: str = "report.html", lang: str = "en"): +def validate_ifc_ids_json(ifc_path: str, ids_path: str, lang: str = "en"): """ - Test an IFC file against an IDS specification and generate reports in multiple formats. + Validate IFC against IDS and return structured JSON data for frontend processing. + This generates the same data that would be used in HTML templates but in JSON format. Args: ifc_path: Path to the IFC file - ids_path: Path to the IDS specification file - report_path: Path where to save the HTML report (default: report.html) - lang: Language for the report (default: en) - Note: Language selection is implemented via post-processing - + ids_path: Path to the IDS specification file + lang: Language code for report (used for metadata only) + Returns: - Dictionary with validation status and paths to generated reports + JSON string with structured validation results """ - print(f"Using language: {lang}") - - # Normalize language code (e.g., 'DE' -> 'de', 'de-de' -> 'de') - lang = lang.lower().split('-')[0] - - # Check if language is supported - if lang not in TRANSLATIONS: - print(f"Warning: Language '{lang}' not supported, falling back to English") - lang = "en" - else: - print(f"Language '{lang}' is supported") - try: # Validate paths if not os.path.exists(ifc_path): @@ -166,83 +181,231 @@ def test_ifc(ifc_path: str, ids_path: str, report_path: str = "report.html", lan print("Validating IFC against IDS...") ids_spec.validate(ifc_model) - # Generate results in multiple formats - results = {} - - # HTML Report - print("Generating HTML report...") - html_reporter = reporter.Html(ids_spec) - html_reporter.report() - html_reporter.to_file(report_path) + # Build structured results that match the HTML template structure + results = { + # Template metadata + "title": f"IDS Validation Report", + "filename": os.path.basename(ifc_path), + "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "language_code": lang, + "_lang": lang, + + # Overall summary statistics + "total_specifications": len(ids_spec.specifications), + "total_specifications_pass": 0, + "total_specifications_fail": 0, + "total_requirements": 0, + "total_requirements_pass": 0, + "total_requirements_fail": 0, + "total_checks": 0, + "total_checks_pass": 0, + "total_checks_fail": 0, + "total_applicable": 0, + "total_applicable_pass": 0, + "total_applicable_fail": 0, + + # Individual specifications + "specifications": [] + } - # Apply translations to HTML - if lang != "en" and lang in TRANSLATIONS: - print(f"Translating report to {lang}...") - with open(report_path, "r", encoding="utf-8") as f: - html_content = f.read() + # Process each specification to match HTML template structure + for spec in ids_spec.specifications: + spec_data = { + "name": spec.name, + "description": getattr(spec, 'description', ''), + "instructions": getattr(spec, 'instructions', ''), + "identifier": getattr(spec, 'identifier', ''), + "ifcVersion": getattr(spec, 'ifcVersion', []), + "is_ifc_version": True, # Assume valid for now + + # Status and statistics + "status": not bool(getattr(spec, 'failed_entities', [])), + "status_text": "PASS" if not getattr(spec, 'failed_entities', []) else "FAIL", + "total_checks": 0, + "total_checks_pass": 0, + "total_checks_fail": 0, + "percent_checks_pass": 0, + "total_applicable": 0, + "total_applicable_pass": 0, + "total_applicable_fail": 0, + + # Applicability and requirements + "applicability": [], + "requirements": [] + } + + # Extract applicability + if hasattr(spec, 'applicability') and spec.applicability: + app_data = extract_applicability_data(spec.applicability) + if app_data: + # Convert to display format for template + app_display = [] + if app_data.get('entity'): + entity_text = f"All {app_data['entity']['name']} data" + if app_data['entity']['predefinedType']: + entity_text += f" with PredefinedType {app_data['entity']['predefinedType']}" + app_display.append(entity_text) + + for prop in app_data.get('property', []): + prop_text = f"Property {prop['baseName']} in set {prop['propertySet']}" + if prop['value']: + prop_text += f" with value {prop['value']}" + app_display.append(prop_text) + + spec_data["applicability"] = app_display + + # Extract requirements + if hasattr(spec, 'requirements') and spec.requirements: + for req in spec.requirements: + req_data = extract_requirement_data(req) + + # Build requirement display data that matches template structure + req_display = { + "description": format_requirement_description(req_data), + "status": req_data["status"] == "pass", + "total_checks": len(req_data.get("failed_entities", [])) + len(req_data.get("passed_entities", [])), + "total_pass": len(req_data.get("passed_entities", [])), + "total_fail": len(req_data.get("failed_entities", [])), + "passed_entities": req_data.get("passed_entities", []), + "failed_entities": req_data.get("failed_entities", []), + "has_omitted_passes": len(req_data.get("passed_entities", [])) > 10, + "has_omitted_failures": len(req_data.get("failed_entities", [])) > 10 + } + + spec_data["requirements"].append(req_display) + + # Update counters + spec_data["total_checks"] += req_display["total_checks"] + spec_data["total_checks_pass"] += req_display["total_pass"] + spec_data["total_checks_fail"] += req_display["total_fail"] - translated_html = translate_html_report(html_content, lang) + # Calculate percentages + if spec_data["total_checks"] > 0: + spec_data["percent_checks_pass"] = round((spec_data["total_checks_pass"] / spec_data["total_checks"]) * 100) - with open(report_path, "w", encoding="utf-8") as f: - f.write(translated_html) + results["specifications"].append(spec_data) + + # Update overall counters + if spec_data["status"]: + results["total_specifications_pass"] += 1 + else: + results["total_specifications_fail"] += 1 + + results["total_checks"] += spec_data["total_checks"] + results["total_checks_pass"] += spec_data["total_checks_pass"] + results["total_checks_fail"] += spec_data["total_checks_fail"] - results["html_path"] = os.path.abspath(report_path) + # Calculate overall percentages + if results["total_checks"] > 0: + results["percent_checks_pass"] = round((results["total_checks_pass"] / results["total_checks"]) * 100) + else: + results["percent_checks_pass"] = 0 + + results["status"] = results["total_specifications_fail"] == 0 + results["status_text"] = "PASS" if results["status"] else "FAIL" - # JSON Report - json_path = report_path.replace('.html', '.json') - print("Generating JSON report...") - json_reporter = reporter.Json(ids_spec) - json_reporter.report() - json_reporter.write(json_path) - results["json_path"] = os.path.abspath(json_path) + # Generate BCF data + print("Generating BCF data...") + try: + bcf_reporter = reporter.Bcf(ids_spec) + bcf_reporter.report() + + # Create BCF in memory + import tempfile + with tempfile.NamedTemporaryFile(suffix='.bcf', delete=False) as tmp_file: + bcf_reporter.to_file(tmp_file.name) + with open(tmp_file.name, 'rb') as f: + bcf_bytes = f.read() + results["bcf_data"] = { + "zip_content": base64.b64encode(bcf_bytes).decode('utf-8'), + "filename": f"{os.path.splitext(os.path.basename(ifc_path))[0]}.bcf" + } + os.unlink(tmp_file.name) # Clean up + except Exception as bcf_error: + print(f"BCF generation failed: {bcf_error}") + results["bcf_data"] = None - # BCF Report - bcf_path = report_path.replace('.html', '.bcf') - print("Generating BCF report...") - bcf_reporter = reporter.Bcf(ids_spec) - bcf_reporter.report() - bcf_reporter.to_file(bcf_path) - results["bcf_path"] = os.path.abspath(bcf_path) + return json.dumps(results, ensure_ascii=False, indent=2) - # Return validation status and report paths - validation_status = "success" if not any(spec.failed_entities for spec in ids_spec.specifications) else "failed" - return { - "status": validation_status, - "reports": results - } except Exception as e: - print(f"Error during IFC testing: {str(e)}") - raise + error_result = { + "error": str(e), + "status": "error", + "title": "Validation Error", + "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + return json.dumps(error_result, ensure_ascii=False) + +def format_requirement_description(req_data): + """Format requirement data into human-readable description""" + req_type = req_data.get("type", "") + + if "Property" in req_type: + if req_data.get("value"): + return f"{req_data.get('baseName', 'Property')} shall be {req_data.get('value')} in the dataset {req_data.get('propertySet', 'PropertySet')}" + else: + return f"{req_data.get('baseName', 'Property')} shall be provided in the dataset {req_data.get('propertySet', 'PropertySet')}" + + elif "Entity" in req_type: + if req_data.get("enumeration"): + return f"The {req_data.get('name', 'Name')} shall be {{{{'enumeration': {req_data.get('enumeration', [])}}}}}" + else: + return f"The {req_data.get('name', 'Name')} shall be {req_data.get('value', '')}" + + elif "Attribute" in req_type: + return f"The {req_data.get('name', 'Attribute')} shall be {req_data.get('value', '')}" + + elif "Classification" in req_type: + return f"Classification {req_data.get('value', '')} in system {req_data.get('system', '')}" + + elif "Material" in req_type: + return f"Material shall be {req_data.get('value', '')}" + + else: + return f"{req_type}: {req_data.get('name', '')} = {req_data.get('value', '')}" def main(): - # Set up argument parser + # Set up argument parser parser = argparse.ArgumentParser(description="IFC Model Checker - Validate IFC files against IDS specifications") parser.add_argument("--ifc", "-i", help="Path to the IFC file") - parser.add_argument("--ids", "-s", help="Path to the IDS specification file") - parser.add_argument("--report", "-r", default="report.html", help="Path for the HTML report (default: report.html)") - parser.add_argument("--lang", "-l", default="en", choices=list(TRANSLATIONS.keys()), - help="Language for the report (default: en)") - parser.add_argument("--interactive", "-int", action="store_true", help="Run in interactive mode") + parser.add_argument("--ids", "-s", help="Path to the IDS specification file") + parser.add_argument("--lang", "-l", default="en", help="Language for the report (default: en)") + parser.add_argument("--json", action="store_true", help="Output JSON instead of running interactively") args = parser.parse_args() - # If interactive mode or no required args, prompt for input - if args.interactive or not (args.ifc and args.ids): - if not args.ifc: - args.ifc = input("Enter path to IFC file: ") - if not args.ids: - args.ids = input("Enter path to IDS file: ") - if not args.report: - args.report = input("Enter path for report (default: report.html): ") or "report.html" - if not args.lang: - args.lang = input(f"Enter language code (default: en, available: {', '.join(TRANSLATIONS.keys())}): ") or "en" - - # Run the validation + # If JSON mode, return structured data for the worker + if args.json and args.ifc and args.ids: + try: + result_json = validate_ifc_ids_json(args.ifc, args.ids, args.lang) + print(result_json) # This will be captured by the worker + except Exception as e: + error_json = json.dumps({"error": str(e)}, ensure_ascii=False) + print(error_json) + return + + # Interactive mode for testing + if not args.ifc: + args.ifc = input("Enter path to IFC file: ") + if not args.ids: + args.ids = input("Enter path to IDS file: ") + if not args.lang: + args.lang = input("Enter language code (default: en): ") or "en" + try: - result = test_ifc(args.ifc, args.ids, args.report, args.lang) - print(f"\nValidation {result['status']}!") - print(f"Generated reports:") - for report_type, path in result['reports'].items(): - print(f"- {report_type}: {path}") + result_json = validate_ifc_ids_json(args.ifc, args.ids, args.lang) + result = json.loads(result_json) + + if "error" in result: + print(f"Error: {result['error']}") + sys.exit(1) + else: + print(f"\nValidation {result['status_text']}!") + print(f"Total specifications: {result['total_specifications']}") + print(f"Passed: {result['total_specifications_pass']}") + print(f"Failed: {result['total_specifications_fail']}") + print(f"Total checks: {result['total_checks']}") + print(f"Success rate: {result['percent_checks_pass']}%") + except Exception as e: print(f"Error: {str(e)}") sys.exit(1) diff --git a/public/report-viewer.html b/public/report-viewer.html new file mode 100644 index 0000000..ac74b84 --- /dev/null +++ b/public/report-viewer.html @@ -0,0 +1,41 @@ + + + + + + Report Viewer + + + +
Loading report...
+ + + + + diff --git a/public/report.html b/public/report.html index a6cc7d7..7593727 100644 --- a/public/report.html +++ b/public/report.html @@ -60,10 +60,14 @@ font-style: italic; } span.item { - padding: 5px; + padding: 4px 10px; border-radius: 5px; - margin-right: 5px; + margin-right: 8px; border: 1px solid #eee; + display: inline-flex; + align-items: center; + gap: 6px; + line-height: 1.2; } span.item.pass, span.item.fail { @@ -138,11 +142,13 @@ border-top: 1px solid #ccc; } summary { - display: flex; + display: list-item; cursor: pointer; + padding: 10px; + font-weight: bold; } - summary::-webkit-details-marker { - display: none; + details[open] summary { + margin-bottom: 10px; } * { box-sizing: border-box; @@ -251,6 +257,28 @@ .percent[data-width='100'] { width: 100%; } + .summary-stats { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + margin: 12px 0; + align-items: center; + } + @media (max-width: 640px) { + .summary-stats { + flex-direction: column; + align-items: flex-start; + } + .summary-stats .item { + width: 100%; + justify-content: space-between; + } + .summary-stats .item.pass, + .summary-stats .item.fail { + width: auto; + justify-content: center; + } + } @@ -266,20 +294,20 @@

{{t.summary}}

class="{{^total_checks}}skipped{{/total_checks}}{{#total_checks}}{{#status}}pass{{/status}}{{^status}}fail{{/status}}{{/total_checks}} percent" data-width="{{^total_checks}}100{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}{{/total_checks}}" > - {{^total_checks}}{{t.skipped}}{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}%{{/total_checks}} + {{^total_checks}}{{t.status.skipped}}{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}%{{/total_checks}} -

+

{{status_text}} - Specifications passed: {{total_specifications_pass}} / + {{t.specificationsPassedPrefix}}: {{total_specifications_pass}} / {{total_specifications}} - Requirements passed: {{total_requirements_pass}} / {{total_requirements}} + {{t.requirementsPassedPrefix}}: {{total_requirements_pass}} / {{total_requirements}} - Checks passed: {{total_checks_pass}} / {{total_checks}} + {{t.checksPassedPrefix}}: {{total_checks_pass}} / {{total_checks}}


@@ -301,28 +329,28 @@

{{name}}

{{^total_checks}}Skipped{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}%{{/total_checks}} -

+

{{^total_checks}}{{t.status.skipped}}{{/total_checks}} {{#total_checks}}{{status_text}}{{/total_checks}} - Checks passed: {{total_checks_pass}} / {{total_checks}} + {{t.checksPassedPrefix}}: {{total_checks_pass}} / {{total_checks}} - Elements passed: {{total_applicable_pass}} / {{total_applicable}} + {{t.elementsPassedPrefix}}: {{total_applicable_pass}} / {{total_applicable}}

{{^is_ifc_version}}

- Warning: specification does not apply to this IFC version + {{t.warning}}: {{t.specNotApplyToVersion}}

{{/is_ifc_version}}

- Applicability + {{t.applicability}}

- Requirements + {{t.requirements}}

    {{#requirements}}
  1. + {{#total_checks}}
    {{description}} - {{#total_ckecks}} - - -
    - {{/total_ckecks}} {{#total_pass}} + {{#total_pass}} @@ -384,9 +409,8 @@

    {{name}}

    - - + @@ -396,23 +420,26 @@

    {{name}}

    - - + {{#extra_of_type}} - + {{/extra_of_type}} {{/failed_entities}} {{#has_omitted_failures}} - + {{/has_omitted_failures}}
    {{t.predefinedType}} {{t.name}} {{t.description}}{{t.warning}} {{t.globalId}}{{t.tag}}{{t.failureReason}}
    {{predefined_type}} {{name}} {{description}}{{reason}} {{global_id}}{{tag}}{{reason}}
    {{t.moreOfSameType}}{{t.moreOfSameType}}
    {{t.moreElementsNotShown}}{{t.moreElementsNotShown}}
    {{/total_fail}}
    + {{/total_checks}} + {{^total_checks}} + {{description}} + {{/total_checks}}
  2. {{/requirements}}
@@ -422,8 +449,11 @@

{{name}}


diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index f106e17..0dc2122 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -14,20 +14,27 @@ export const Footer = () => { {t('sponsored-by')} - Righetti and Partner + + Righetti and Partner + {t('open-source')} diff --git a/src/components/IDSReport/HTMLTemplateRenderer.tsx b/src/components/IDSReport/HTMLTemplateRenderer.tsx new file mode 100644 index 0000000..76b1d12 --- /dev/null +++ b/src/components/IDSReport/HTMLTemplateRenderer.tsx @@ -0,0 +1,377 @@ +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { IDSTranslationService, ValidationResult } from '../../services/IDSTranslationService' + +interface HTMLTemplateRendererProps { + validationResults: ValidationResult + onReportGenerated: (htmlContent: string) => void +} + +/** + * Component that generates HTML reports using the exact same template structure + * as the current system, but with proper translation support + */ +export const HTMLTemplateRenderer: React.FC = ({ validationResults, onReportGenerated }) => { + const { t, i18n } = useTranslation() + const [templateContent, setTemplateContent] = useState(null) + + // Load the HTML template + useEffect(() => { + fetch('/report.html') + .then((response) => response.text()) + .then((content) => { + setTemplateContent(content) + }) + .catch((error) => console.error('Error loading template:', error)) + }, []) + + // Generate the report when template and results are ready + useEffect(() => { + if (templateContent && validationResults) { + generateReport() + } + }, [templateContent, validationResults, i18n.language]) + + const generateReport = () => { + if (!templateContent) return + + // Create translation service + const translationService = new IDSTranslationService(t) + + // Translate the validation results + const translatedResults = translationService.translateValidationResults(validationResults) + + // Prepare template data with translations + const templateData = { + ...translatedResults, + + // Add translation object for template use + t: { + summary: t('report.summary', 'Summary'), + specifications: t('report.specifications', 'Specifications'), + requirements: t('report.requirements', 'Requirements'), + details: t('report.details', 'Details'), + class: t('report.class', 'Class'), + predefinedType: t('report.predefinedType', 'PredefinedType'), + name: t('report.name', 'Name'), + description: t('report.description', 'Description'), + warning: t('report.warning', 'Warning'), + globalId: t('report.globalId', 'GlobalId'), + tag: t('report.tag', 'Tag'), + reportBy: t('report.reportBy', 'Report by'), + and: t('report.and', 'and'), + status: { + pass: t('report.status.pass', 'PASS'), + fail: t('report.status.fail', 'FAIL'), + untested: t('report.status.untested', 'UNTESTED'), + skipped: t('report.status.skipped', 'SKIPPED'), + }, + skipped: t('report.status.skipped', 'SKIPPED'), + moreOfSameType: t( + 'report.phrases.moreOfSameType', + '... {{count}} more of the same element type ({{type}} with Tag {{tag}} and GlobalId {{id}}) not shown ...', + ), + moreElementsNotShown: t( + 'report.phrases.moreElementsNotShown', + '... {{count}} more {{type}} elements not shown out of {{total}} total ...', + ), + }, + } + + // Use Mustache-like template replacement (simplified version) + let htmlContent = templateContent + + // Replace template variables + htmlContent = replaceMustacheVariables(htmlContent, templateData) + + // Apply additional translations for dynamic content + htmlContent = applyDynamicTranslations(htmlContent, translationService) + + // Callback with the generated HTML + onReportGenerated(htmlContent) + } + + /** + * Replace Mustache-style template variables with actual values + */ + const replaceMustacheVariables = (template: string, data: any): string => { + let result = template + + // Simple variable replacement + const simpleVars = [ + 'title', + 'filename', + 'date', + '_lang', + 'total_specifications', + 'total_specifications_pass', + 'total_specifications_fail', + 'total_requirements', + 'total_requirements_pass', + 'total_requirements_fail', + 'total_checks', + 'total_checks_pass', + 'total_checks_fail', + 'percent_checks_pass', + 'status_text', + ] + + simpleVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + result = result.replace(regex, data[varName] || '') + }) + + // Translation variables + const translationVars = [ + 't.summary', + 't.specifications', + 't.requirements', + 't.details', + 't.class', + 't.predefinedType', + 't.name', + 't.description', + 't.warning', + 't.globalId', + 't.tag', + 't.reportBy', + 't.and', + 't.status.pass', + 't.status.fail', + 't.status.untested', + 't.status.skipped', + 't.skipped', + 't.moreOfSameType', + 't.moreElementsNotShown', + ] + + translationVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + const value = getNestedValue(data, varName) + result = result.replace(regex, value || '') + }) + + // Handle conditional sections and loops + result = processConditionalSections(result, data) + result = processSpecificationLoops(result, data) + + return result + } + + /** + * Get nested object value by dot notation + */ + const getNestedValue = (obj: any, path: string): any => { + return path.split('.').reduce((current, key) => current?.[key], obj) + } + + /** + * Process conditional sections in the template + */ + const processConditionalSections = (template: string, data: any): string => { + let result = template + + // Handle status conditionals + if (data.status) { + result = result.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, 'pass') + result = result.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, '') + } else { + result = result.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, '') + result = result.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, 'fail') + } + + // Handle total_checks conditionals + if (data.total_checks > 0) { + result = result.replace(/\{\{\^total_checks\}\}.*?\{\{\/total_checks\}\}/g, '') + result = result.replace(/\{\{#total_checks\}\}/g, '') + result = result.replace(/\{\{\/total_checks\}\}/g, '') + } else { + result = result.replace(/\{\{#total_checks\}\}.*?\{\{\/total_checks\}\}/g, '') + result = result.replace(/\{\{\^total_checks\}\}/g, '') + result = result.replace(/\{\{\/total_checks\}\}/g, '') + } + + return result + } + + /** + * Process specification loops in the template + */ + const processSpecificationLoops = (template: string, data: any): string => { + let result = template + + // Find and replace specification loop + const specLoopRegex = /\{\{#specifications\}\}([\s\S]*?)\{\{\/specifications\}\}/g + const specTemplate = template.match(specLoopRegex)?.[0] + + if (specTemplate && data.specifications) { + const specContent = specTemplate.replace(/\{\{#specifications\}\}/, '').replace(/\{\{\/specifications\}\}/, '') + + const specHtml = data.specifications + .map((spec: any) => { + let specSection = specContent + + // Replace specification variables + const specVars = [ + 'name', + 'description', + 'instructions', + 'status_text', + 'total_checks', + 'total_checks_pass', + 'total_checks_fail', + 'percent_checks_pass', + ] + + specVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + specSection = specSection.replace(regex, spec[varName] || '') + }) + + // Handle applicability loop + if (spec.applicability && spec.applicability.length > 0) { + const appLoopRegex = /\{\{#applicability\}\}([\s\S]*?)\{\{\/applicability\}\}/g + const appTemplate = specSection.match(appLoopRegex)?.[0] + + if (appTemplate) { + const appContent = appTemplate + .replace(/\{\{#applicability\}\}/, '') + .replace(/\{\{\/applicability\}\}/, '') + const appHtml = spec.applicability.map((app: string) => appContent.replace(/\{\{\.\}\}/g, app)).join('') + + specSection = specSection.replace(appLoopRegex, appHtml) + } + } + + // Handle requirements loop + if (spec.requirements && spec.requirements.length > 0) { + const reqLoopRegex = /\{\{#requirements\}\}([\s\S]*?)\{\{\/requirements\}\}/g + const reqTemplate = specSection.match(reqLoopRegex)?.[0] + + if (reqTemplate) { + const reqContent = reqTemplate.replace(/\{\{#requirements\}\}/, '').replace(/\{\{\/requirements\}\}/, '') + const reqHtml = spec.requirements + .map((req: any) => { + let reqSection = reqContent + + // Replace requirement variables + const reqVars = ['description', 'total_checks', 'total_pass', 'total_fail'] + reqVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + reqSection = reqSection.replace(regex, req[varName] || '') + }) + + // Handle entity tables (passed and failed) + reqSection = processEntityTables(reqSection, req) + + return reqSection + }) + .join('') + + specSection = specSection.replace(reqLoopRegex, reqHtml) + } + } + + // Handle specification status conditionals + if (spec.status) { + specSection = specSection.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, 'pass') + specSection = specSection.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, '') + } else { + specSection = specSection.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, '') + specSection = specSection.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, 'fail') + } + + return specSection + }) + .join('') + + result = result.replace(specLoopRegex, specHtml) + } + + return result + } + + /** + * Process entity tables for passed and failed entities + */ + const processEntityTables = (template: string, req: any): string => { + let result = template + + // Process passed entities table + if (req.passed_entities && req.passed_entities.length > 0) { + const passedTableRegex = /\{\{#total_pass\}\}([\s\S]*?)\{\{\/total_pass\}\}/g + const passedTemplate = result.match(passedTableRegex)?.[0] + + if (passedTemplate) { + const tableContent = passedTemplate.replace(/\{\{#total_pass\}\}/, '').replace(/\{\{\/total_pass\}\}/, '') + const entityRows = req.passed_entities + .map( + (entity: any) => + ` + ${entity.type} + ${entity.predefinedType || ''} + ${entity.name} + ${entity.description} + ${entity.globalId} + ${entity.tag} + `, + ) + .join('') + + const tableHtml = tableContent.replace(/[\s\S]*?<\/tbody>/, `${entityRows}`) + + result = result.replace(passedTableRegex, tableHtml) + } + } else { + result = result.replace(/\{\{#total_pass\}\}[\s\S]*?\{\{\/total_pass\}\}/g, '') + } + + // Process failed entities table + if (req.failed_entities && req.failed_entities.length > 0) { + const failedTableRegex = /\{\{#total_fail\}\}([\s\S]*?)\{\{\/total_fail\}\}/g + const failedTemplate = result.match(failedTableRegex)?.[0] + + if (failedTemplate) { + const tableContent = failedTemplate.replace(/\{\{#total_fail\}\}/, '').replace(/\{\{\/total_fail\}\}/, '') + const entityRows = req.failed_entities + .map( + (entity: any) => + ` + ${entity.type} + ${entity.predefinedType || ''} + ${entity.name} + ${entity.description} + Warning message here + ${entity.globalId} + ${entity.tag} + `, + ) + .join('') + + const tableHtml = tableContent.replace(/[\s\S]*?<\/tbody>/, `${entityRows}`) + + result = result.replace(failedTableRegex, tableHtml) + } + } else { + result = result.replace(/\{\{#total_fail\}\}[\s\S]*?\{\{\/total_fail\}\}/g, '') + } + + return result + } + + /** + * Apply additional dynamic translations that couldn't be handled by template variables + */ + const applyDynamicTranslations = (html: string, translationService: IDSTranslationService): string => { + let result = html + + // Apply any remaining pattern-based translations + // This is where we can add more sophisticated translation logic if needed + + return result + } + + return null // This component doesn't render anything visible +} + +export default HTMLTemplateRenderer diff --git a/src/components/ThemeToggle/ThemeToggle.css b/src/components/ThemeToggle/ThemeToggle.css index 53bb882..728262b 100644 --- a/src/components/ThemeToggle/ThemeToggle.css +++ b/src/components/ThemeToggle/ThemeToggle.css @@ -35,7 +35,8 @@ } .sun-icon, -.moon-icon { +.moon-icon, +.system-icon { position: absolute; top: 0; left: 0; @@ -56,6 +57,13 @@ color: #7aa2f7; /* Tokyo Night blue */ } +/* System icon defaults hidden; shown when wrapper has .system */ +.system-icon { + opacity: 0; + transform: rotate(-90deg) scale(0); + color: var(--text-primary); +} + .dark .sun-icon { opacity: 0; transform: rotate(90deg) scale(0); @@ -65,3 +73,15 @@ opacity: 1; transform: rotate(0) scale(1); } + +/* When explicitly in system mode, show only the system icon */ +.system .sun-icon, +.system .moon-icon { + opacity: 0; + transform: rotate(90deg) scale(0); +} + +.system .system-icon { + opacity: 1; + transform: rotate(0) scale(1); +} diff --git a/src/components/ThemeToggle/ThemeToggle.tsx b/src/components/ThemeToggle/ThemeToggle.tsx index ca826cc..813cdd2 100644 --- a/src/components/ThemeToggle/ThemeToggle.tsx +++ b/src/components/ThemeToggle/ThemeToggle.tsx @@ -1,20 +1,23 @@ import { useTheme } from '@context' -import { IconSun, IconMoon } from '@tabler/icons-react' +import { IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-react' import './ThemeToggle.css' export const ThemeToggle = () => { - const { isDarkMode, toggleTheme } = useTheme() + const { mode, isDarkMode, setTheme } = useTheme() + + const cycleMode = () => { + if (mode === 'system') setTheme('light') + else if (mode === 'light') setTheme('dark') + else setTheme('system') + } return ( - diff --git a/src/components/UploadCard/UploadCard.tsx b/src/components/UploadCard/UploadCard.tsx index c5047eb..9f964c9 100644 --- a/src/components/UploadCard/UploadCard.tsx +++ b/src/components/UploadCard/UploadCard.tsx @@ -5,6 +5,7 @@ import { useEffect, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { BcfData, downloadBcfReport } from '../../utils/bcfUtils' +import { ValidationResult } from '../../types/validation' import { processFile } from './processFile.ts' import { ErrorDisplay, @@ -15,7 +16,8 @@ import { UploadInstructions, } from './components' import { UploadCardTitle } from './UploadCardTitle.tsx' -import { useFileProcessor, useHtmlReport } from './hooks' +import { useFileProcessor } from './hooks' +import { useEnhancedHtmlReport } from './hooks/useEnhancedHtmlReport' import { FileError } from './hooks/useFileProcessor' export const UploadCard = () => { @@ -53,7 +55,7 @@ export const UploadCard = () => { reportFormats, }) - const { openHtmlReport } = useHtmlReport(templateContent, i18n) + const { openHtmlReport, downloadHtmlReport } = useEnhancedHtmlReport(templateContent, i18n) // Function to scroll to results const scrollToResults = () => { @@ -77,13 +79,16 @@ export const UploadCard = () => { useEffect(() => { // Load the HTML template - fetch('/report.html') - .then((response) => response.text()) - .then((content) => { - console.log('Template loaded, length:', content.length) + const loadTemplate = async () => { + try { + const response = await fetch('/report.html') + const content = await response.text() setTemplateContent(content) - }) - .catch((error) => console.error('Error loading template:', error)) + } catch (error) { + console.error('Failed to load template:', error) + } + } + loadTemplate() }, []) const handleBcfDownload = (result: { @@ -104,6 +109,15 @@ export const UploadCard = () => { } } + const handleHtmlDownload = async (result: ValidationResult, fileName: string) => { + try { + await downloadHtmlReport(result, fileName) + } catch (error) { + console.error('Failed to download HTML report:', error) + setUploadError(`Failed to download HTML report: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + } + const handleClick = async () => { setUploadError(null) setProcessedResults([]) @@ -178,6 +192,7 @@ export const UploadCard = () => { processedResults={processedResults} reportFormats={reportFormats} onHtmlReport={openHtmlReport} + onHtmlDownload={handleHtmlDownload} onBcfDownload={handleBcfDownload} resultsRef={resultsRef} /> diff --git a/src/components/UploadCard/components/ProcessingConsole.tsx b/src/components/UploadCard/components/ProcessingConsole.tsx index acf0c71..c4d8b9f 100644 --- a/src/components/UploadCard/components/ProcessingConsole.tsx +++ b/src/components/UploadCard/components/ProcessingConsole.tsx @@ -1,5 +1,5 @@ import { Group, Paper, ScrollArea, Stack, Text } from '@mantine/core' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' interface ProcessingConsoleProps { @@ -25,6 +25,7 @@ const consoleStyles = { export const ProcessingConsole = ({ isProcessing, logs }: ProcessingConsoleProps) => { const { t } = useTranslation() const [loadingDots, setLoadingDots] = useState('') + const bottomRef = useRef(null) useEffect(() => { if (isProcessing) { @@ -35,6 +36,15 @@ export const ProcessingConsole = ({ isProcessing, logs }: ProcessingConsoleProps } }, [isProcessing]) + // Auto-scroll to the latest log entry whenever logs update or processing state changes + useEffect(() => { + // Slight delay ensures DOM has rendered the new log before scrolling + const id = setTimeout(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) + }, 0) + return () => clearTimeout(id) + }, [logs.length, isProcessing]) + if (!isProcessing && logs.length === 0) { return null } @@ -77,6 +87,7 @@ export const ProcessingConsole = ({ isProcessing, logs }: ProcessingConsoleProps {loadingDots} )} +
diff --git a/src/components/UploadCard/components/ResultsDisplay.tsx b/src/components/UploadCard/components/ResultsDisplay.tsx index f4975c5..bfecbd1 100644 --- a/src/components/UploadCard/components/ResultsDisplay.tsx +++ b/src/components/UploadCard/components/ResultsDisplay.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Group, Text } from '@mantine/core' +import { Alert, Button, Group, Text, Tooltip } from '@mantine/core' import { IconDownload, IconFileText } from '@tabler/icons-react' import { useTranslation } from 'react-i18next' import { useEffect, RefObject } from 'react' @@ -13,6 +13,7 @@ interface ResultsDisplayProps { bcf: boolean } onHtmlReport: (result: ValidationResult, fileName: string) => void + onHtmlDownload: (result: ValidationResult, fileName: string) => Promise onBcfDownload: (result: { fileName: string result: { @@ -26,6 +27,7 @@ export const ResultsDisplay = ({ processedResults, reportFormats, onHtmlReport, + onHtmlDownload, onBcfDownload, resultsRef, }: ResultsDisplayProps) => { @@ -67,57 +69,93 @@ export const ResultsDisplay = ({ {processedResults.map((result, index) => ( {reportFormats.html && ( - + }} + > + View HTML - {result.fileName} + + + + + )} {reportFormats.bcf && (