diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9026a5c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 peeb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 55591bc..6980a0e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,13 @@ We recommend Docker Desktop for managing the images and containers. (Link: https # How to run the platform: +### running on ramen + +`sudo docker compose down -v` +`sudo docker compose build frontend` +`sudo docker compose up -d` +`sudo docker compose exec backend python manage.py migrate` + ### building images Inside the folder containing the repository, please run the following command: @@ -49,12 +56,4 @@ Alternatively, if you have the docker desktop app, you can follow these steps: ### You're done! You can now access the platform at: localhost:3000 -### MIT Liscense - -Copyright (c) <2024> - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/compose.yaml b/compose.yaml index 85c209e..6d109d5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,8 +6,15 @@ services: ports: - "8000:8000" user: root + restart: unless-stopped + healthcheck: + test: [ "CMD-SHELL", "curl -f http://localhost:8000/ || kill 1" ] + interval: 60s + timeout: 20s + retries: 5 volumes: - /var/run/docker.sock:/var/run/docker.sock + - ./platform/backend:/app - ./media:/app/mysite/media depends_on: - db @@ -17,15 +24,24 @@ services: frontend: build: context: ./platform/frontend + restart: unless-stopped + healthcheck: + test: [ "CMD-SHELL", "wget -qO- http://localhost:3000/ > /dev/null || kill 1" ] + interval: 60s + timeout: 20s + retries: 3 volumes: - ./platform/frontend:/app - /app/node_modules ports: - - "3000:3000" + - "8080:3000" working_dir: /app - command: npm run dev - environment: - - CHOKIDAR_USEPOLLING=true + command: + - sh + - -c + - | + npm run build + npm start # Dieser Dienst ist jetzt AKTIVIERT. # Er läuft nicht permanent, aber Docker Compose weiß jetzt, wie man @@ -33,12 +49,15 @@ services: worker: build: context: ./worker - image: exact-worker # Wir definieren den Namen des Images explizit + image: exact-worker # Wir definieren den Namen des Images explizit + platform: linux/amd64 + user: root + volumes: + - /var/run/docker.sock:/var/run/docker.sock db: - image: postgres + image: postgres:16 restart: always - user: postgres secrets: - db-password volumes: @@ -47,11 +66,11 @@ services: - POSTGRES_DB=example - POSTGRES_PASSWORD_FILE=/run/secrets/db-password ports: - - "5432:5432" + - "5433:5432" healthcheck: - test: ["CMD", "pg_isready"] - interval: 10s - timeout: 5s + test: [ "CMD", "pg_isready" ] + interval: 60s + timeout: 10s retries: 5 devcontainer: @@ -67,4 +86,4 @@ volumes: secrets: db-password: - file: db/password.txt \ No newline at end of file + file: db/password.txt diff --git a/create_issues.sh b/create_issues.sh new file mode 100755 index 0000000..2e1b8ec --- /dev/null +++ b/create_issues.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +REPO="braindatalab/exact" +CSV_FILE="issues.csv" + +# Skip header +tail -n +1 "$CSV_FILE" | while IFS=',' read -r raw_title raw_body raw_labels; do + # Remove surrounding quotes and clean up spacing + title=$(echo "$raw_title" | sed 's/^"//;s/"$//' | xargs) + body=$(echo "$raw_body" | sed 's/^"//;s/"$//' | xargs) + labels=$(echo "$raw_labels" | sed 's/^"//;s/"$//' | xargs) + + # Write body to temp file + tmpfile=$(mktemp) + echo "$body" > "$tmpfile" + + echo "🛠 Creating issue: $title" + + # Build label flags + label_args=() + IFS=',' read -ra label_array <<< "$labels" + for label in "${label_array[@]}"; do + label_args+=(--label "$label") + done + + # Create the issue + gh issue create \ + --repo "$REPO" \ + --title "$title" \ + --body-file "$tmpfile" \ + "${label_args[@]}" + + rm "$tmpfile" + + echo "✅ Created: $title" +done diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..1e4e911 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,4 @@ +#!/bin/bash +sudo docker compose down +sudo docker compose up -d --build +sudo docker compose exec backend python manage.py migrate \ No newline at end of file diff --git a/issues.csv b/issues.csv new file mode 100644 index 0000000..4ffa46e --- /dev/null +++ b/issues.csv @@ -0,0 +1,17 @@ +"Define core user stories for the platform MVP","Create high-level user stories for registering, submitting a method, viewing leaderboard, and administering challenges.","planning,meta" +"Update user registration to require admin approval","Add a registration status field (e.g., pending, approved), create admin UI to approve users, and prevent unapproved users from accessing submission features.","frontend,backend,database" +"Design and store method name field for submissions","Add form field for method name in submission UI, store in DB, and expose via API.","frontend,backend,database" +"Display method names on leaderboard","Update leaderboard UI and API to show submitted method names alongside scores.","frontend,backend" +"Enable sorting leaderboard by method score","Allow users to click table headers to sort by score; add support for sorting server-side or client-side.","frontend,backend" +"Update leaderboard to show all metric results per submission","Show full list of metrics per method: EMD, IMA, etc. alongside method name and challenge.","frontend,backend" +"Enable leaderboard sorting by each metric, default to primary metric","Add column-based sorting to each metric; default sort by user-selected or predefined metric.","frontend,backend" +"Add metric selection (checkboxes) to challenge creation UI","Let admins select metrics (e.g. EMD, IMA) when creating a challenge; store this in DB.","frontend,backend" +"Create structure for XAI super-challenges","Group related scenarios (e.g. XAI-TRIS variants) under one meta-challenge with shared leaderboard and/or unified submission logic.","backend,database" +"Integrate all XAI-TRIS datasets as challenges","For each XAI-TRIS scenario, create challenge metadata, upload dataset, and add basic visualization/description.","backend,worker,database" +"Integrate MRI dataset into the platform","Add MRI semi-synthetic dataset as a challenge, with description and appropriate metrics.","backend,worker,database" +"Enable multi-worker processing system","Modify job dispatch system to handle concurrent/multiple workers processing submissions in parallel.","worker,backend" +"Implement full input/output validation and Docker sandboxing","Validate submission format, restrict Docker execution, check method outputs conform to required interface.","worker,security,backend" +"Add CI pipeline with GitHub Actions","Set up CI to lint, test backend/frontend, and verify Docker builds.","devops" +"Test deployment on PTB server","Ensure full system deploys on PTB infrastructure, including DB, backend, frontend, and workers.","devops,deployment" +"Platform stabilization and bug fixing sprint","Focus sprint on UI polish, error handling, and cross-component reliability.","frontend,backend,worker,qa" +"Populate known results from XAI-TRIS and MRI papers","Add historical experimental results to leaderboard for baseline comparisons.","backend,database" diff --git a/platform/backend/api/forms.py b/platform/backend/api/forms.py index cd83e3b..f938e2b 100644 --- a/platform/backend/api/forms.py +++ b/platform/backend/api/forms.py @@ -12,6 +12,7 @@ class ChallengeForm(forms.Form): widget=forms.Textarea, # mehrzeiliges Textfeld max_length=100 # optional: Längenlimit ) + creator = forms.CharField(required=False, max_length=150) xai_method = forms.FileField() dataset = forms.FileField() mlmodel = forms.FileField() diff --git a/platform/backend/api/migrations/0002_auto_20260325_2147.py b/platform/backend/api/migrations/0002_auto_20260325_2147.py new file mode 100644 index 0000000..379fe7f --- /dev/null +++ b/platform/backend/api/migrations/0002_auto_20260325_2147.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.23 on 2026-03-25 21:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='score', + name='emd_score', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='score', + name='emd_std', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='score', + name='error_message', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='score', + name='ima_score', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='score', + name='ima_std', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='score', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed')], default='completed', max_length=20), + ), + migrations.AlterField( + model_name='score', + name='score', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/platform/backend/api/migrations/0003_challenge_creator.py b/platform/backend/api/migrations/0003_challenge_creator.py new file mode 100644 index 0000000..17dc1a3 --- /dev/null +++ b/platform/backend/api/migrations/0003_challenge_creator.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2026-05-20 08:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_auto_20260325_2147'), + ] + + operations = [ + migrations.AddField( + model_name='challenge', + name='creator', + field=models.CharField(blank=True, max_length=150, null=True), + ), + ] diff --git a/platform/backend/api/migrations/0004_score_plot_base64.py b/platform/backend/api/migrations/0004_score_plot_base64.py new file mode 100644 index 0000000..0730ac1 --- /dev/null +++ b/platform/backend/api/migrations/0004_score_plot_base64.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2026-05-20 08:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_challenge_creator'), + ] + + operations = [ + migrations.AddField( + model_name='score', + name='plot_base64', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/platform/backend/api/models.py b/platform/backend/api/models.py index 78cf428..0ef603d 100644 --- a/platform/backend/api/models.py +++ b/platform/backend/api/models.py @@ -37,6 +37,8 @@ class Score(models.Model): ima_score = models.FloatField(null=True, blank=True) ima_std = models.FloatField(null=True, blank=True) + plot_base64 = models.TextField(null=True, blank=True) + method_name = models.CharField(max_length=100, blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) @@ -76,11 +78,11 @@ def __str__(self): score_display = " | ".join(scores_str) if scores_str else "No scores" return f"User {self.username} - Challenge {self.challenge_id} - {score_display}" -# Challenge model (bleibt unverändert) class Challenge(models.Model): challenge_id = models.CharField(max_length=100, unique=True, editable=False) title = models.CharField(max_length=100) description = models.TextField() + creator = models.CharField(max_length=150, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) # Uploads diff --git a/platform/backend/api/serializers.py b/platform/backend/api/serializers.py index 845e703..7135f6c 100644 --- a/platform/backend/api/serializers.py +++ b/platform/backend/api/serializers.py @@ -10,6 +10,8 @@ class ScoreSerializer(serializers.ModelSerializer): ima_score = serializers.FloatField(required=False, allow_null=True) ima_std = serializers.FloatField(required=False, allow_null=True) + plot_base64 = serializers.CharField(required=False, allow_null=True) + # Legacy score field - now optional score = serializers.FloatField(required=False, allow_null=True) @@ -27,6 +29,7 @@ class Meta: 'emd_std', 'ima_score', 'ima_std', + 'plot_base64', 'primary_score', 'method_name', 'created_at', @@ -48,6 +51,7 @@ class Meta: 'challenge_id', 'title', 'description', + 'creator', 'created_at', 'dataset', 'mlmodel', diff --git a/platform/backend/api/views.py b/platform/backend/api/views.py index d210004..4a8ede7 100644 --- a/platform/backend/api/views.py +++ b/platform/backend/api/views.py @@ -1,9 +1,11 @@ import uuid import os -from rest_framework.decorators import api_view, parser_classes +from rest_framework.decorators import api_view, parser_classes, authentication_classes, permission_classes from rest_framework.response import Response from rest_framework.parsers import MultiPartParser from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.authentication import SessionAuthentication from .models import * from .serializers import * from .worker_utils import spawn_worker_container @@ -20,6 +22,8 @@ @api_view(['POST']) @parser_classes([MultiPartParser]) +@authentication_classes([SessionAuthentication]) +@permission_classes([IsAuthenticated]) def xai_detail(request, challenge_id): """ Process XAI method submission and calculate both EMD and IMA scores. @@ -29,7 +33,6 @@ def xai_detail(request, challenge_id): return Response({'error': 'Invalid form submission.'}, status=status.HTTP_400_BAD_REQUEST) input_file = form.cleaned_data['file'] - username = form.cleaned_data['username'] method_name = form.cleaned_data.get('method_name', 'Unnamed Method') xai_method_code = input_file.read().decode('utf-8') @@ -41,13 +44,14 @@ def xai_detail(request, challenge_id): score_data = { 'challenge_id': challenge_id, - 'username': username, + 'username': request.user.username, 'method_name': method_name, 'status': 'completed', 'emd_score': scores.get('emd_score'), 'emd_std': scores.get('emd_std'), 'ima_score': scores.get('ima_score'), 'ima_std': scores.get('ima_std'), + 'plot_base64': scores.get('plot_base64'), 'score': scores.get('emd_score') # Standard-Score ist EMD } @@ -173,6 +177,8 @@ def xaimethod_detail(request, challenge_id): return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['POST']) +@authentication_classes([SessionAuthentication]) +@permission_classes([IsAuthenticated]) def create_challenge(request): if request.method == 'POST': form = ChallengeForm(request.POST, request.FILES) @@ -182,6 +188,7 @@ def create_challenge(request): challenge_id=unique_id, title=form.cleaned_data['title'], description=form.cleaned_data['description'], + creator=request.user.username, xaimethod=form.cleaned_data['xai_method'], dataset=form.cleaned_data['dataset'], mlmodel=form.cleaned_data['mlmodel'], @@ -192,6 +199,8 @@ def create_challenge(request): @csrf_exempt def challenge_form_view(request): + if not request.user.is_authenticated: + return HttpResponse("Unauthorized", status=401) if request.method == 'POST': form = ChallengeForm(request.POST, request.FILES) if form.is_valid(): @@ -200,6 +209,7 @@ def challenge_form_view(request): challenge_id=unique_id, title=form.cleaned_data['title'], description=form.cleaned_data['description'], + creator=request.user.username, xaimethod=form.cleaned_data['xai_method'], dataset=form.cleaned_data['dataset'], mlmodel=form.cleaned_data['mlmodel'], @@ -220,6 +230,19 @@ def get_challenge(request, challenge_id): return Response(serializer.data) except Challenge.DoesNotExist: return Response({"error": "Challenge not found"}, status=404) + +@api_view(['DELETE']) +@authentication_classes([SessionAuthentication]) +@permission_classes([IsAuthenticated]) +def delete_challenge(request, challenge_id): + try: + challenge = Challenge.objects.get(challenge_id=challenge_id) + if challenge.creator and challenge.creator != request.user.username: + return Response({"error": "Unauthorized"}, status=403) + challenge.delete() + return Response({"message": "Deleted successfully"}, status=200) + except Challenge.DoesNotExist: + return Response({"error": "Challenge not found"}, status=404) @api_view(['GET']) def get_challenges(request): diff --git a/platform/backend/api/worker_utils.py b/platform/backend/api/worker_utils.py index 0168924..087d98b 100644 --- a/platform/backend/api/worker_utils.py +++ b/platform/backend/api/worker_utils.py @@ -16,6 +16,11 @@ def parse_scores_from_logs(logs: str): final_score_match = re.search(r'FINAL_SCORE:([0-9.]+)', logs) if final_score_match: scores['mean'] = float(final_score_match.group(1)) + + plot_match = re.search(r'PLOT_DATA_START:(.*?):PLOT_DATA_END', logs, re.DOTALL) + if plot_match: + scores['plot_base64'] = plot_match.group(1) + return scores def run_metric_in_container(client: docker.DockerClient, worker_image_id: str, command: list, environment: dict): @@ -33,21 +38,25 @@ def run_metric_in_container(client: docker.DockerClient, worker_image_id: str, c logger.info(f"Container logs for '{' '.join(command)}':\n{logs}") if result['StatusCode'] == 0: - return parse_scores_from_logs(logs) + scores = parse_scores_from_logs(logs) + if scores.get('mean') is None: + return {'mean': None, 'std': None, 'error': f"Could not parse scores from logs. Output was:\n{logs}"} + return scores else: logger.error(f"Container for command '{' '.join(command)}' failed with status code {result['StatusCode']}.") - return {'mean': None, 'std': None} + return {'mean': None, 'std': None, 'error': f"Container failed with status code {result['StatusCode']}. Output was:\n{logs}"} except docker.errors.ContainerError as e: - logger.error(f"ContainerError during '{' '.join(command)}'. Logs:\n{e.container.logs().decode('utf-8')}") - return {'mean': None, 'std': None} + logs = e.container.logs().decode('utf-8') + logger.error(f"ContainerError during '{' '.join(command)}'. Logs:\n{logs}") + return {'mean': None, 'std': None, 'error': f"ContainerError: {logs}"} except Exception as e: logger.error(f"An unexpected error occurred during '{' '.join(command)}': {e}") - return {'mean': None, 'std': None} + return {'mean': None, 'std': None, 'error': f"Unexpected error: {e}"} def spawn_worker_container(worker_id: str, challenge_id: str, xai_method: str): logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - final_scores = {'emd_score': None, 'emd_std': None, 'ima_score': None, 'ima_std': None} + final_scores = {'emd_score': None, 'emd_std': None, 'ima_score': None, 'ima_std': None, 'plot_base64': None} try: client = docker.from_env() @@ -65,13 +74,19 @@ def spawn_worker_container(worker_id: str, challenge_id: str, xai_method: str): # Führe EMD-Berechnung aus emd_command = ["python", "emd.py"] emd_results = run_metric_in_container(client, worker_image.id, emd_command, base_environment) + if emd_results and emd_results.get('error'): + return (f"EMD Error: {emd_results['error']}", final_scores) if emd_results: final_scores['emd_score'] = emd_results.get('mean') final_scores['emd_std'] = emd_results.get('std') + if emd_results.get('plot_base64'): + final_scores['plot_base64'] = emd_results.get('plot_base64') # Führe IMA-Berechnung aus ima_command = ["python", "ima.py"] ima_results = run_metric_in_container(client, worker_image.id, ima_command, base_environment) + if ima_results and ima_results.get('error'): + return (f"IMA Error: {ima_results['error']}", final_scores) if ima_results: final_scores['ima_score'] = ima_results.get('mean') final_scores['ima_std'] = ima_results.get('std') diff --git a/platform/backend/mysite/settings.py b/platform/backend/mysite/settings.py index 6b92486..7e324a0 100644 --- a/platform/backend/mysite/settings.py +++ b/platform/backend/mysite/settings.py @@ -30,9 +30,8 @@ ALLOWED_HOSTS = ['*'] -CORS_ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://127.0.0.1:3000' +CORS_ALLOWED_ORIGIN_REGEXES = [ + r"^.*$" ] CORS_ALLOW_CREDENTIALS = True diff --git a/platform/backend/mysite/urls.py b/platform/backend/mysite/urls.py index 0650c84..9a87333 100644 --- a/platform/backend/mysite/urls.py +++ b/platform/backend/mysite/urls.py @@ -22,19 +22,18 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('api/xai//', xai_detail), # For submitting XAI-methods and getting the computed score - path('api/score//', score_detail), - path('api/dataset//', dataset_detail), - path('api/mlmodel//', mlmodel_detail), - path('api/xaimethod//', xaimethod_detail), - path('api/challenge/create/', create_challenge), - #path('api/challenge//delete/', delete_challenge), # New URL for deleting a challenge - path('challenge/form', challenge_form_view), # New URL pattern for the form, + path('api/xai/', xai_detail), # For submitting XAI-methods and getting the computed score + path('api/score/', score_detail), + path('api/dataset/', dataset_detail), + path('api/mlmodel/', mlmodel_detail), + path('api/xaimethod/', xaimethod_detail), + path('api/challenge/create', create_challenge), + path('challenge/form', challenge_form_view), # Form already didn't have it path('success/', success_view, name='success'), - path('api/challenge//', get_challenge), - path('api/challenges/', get_challenges), - # path('api/newscore/', add_score), - path('api/scores/', get_scores), + path('api/challenge/', get_challenge), + path('api/challenge//delete', delete_challenge), + path('api/challenges', get_challenges), + path('api/scores', get_scores), path('', include('user_api.urls')), ] diff --git a/platform/backend/user_api/migrations/0001_initial.py b/platform/backend/user_api/migrations/0001_initial.py new file mode 100644 index 0000000..abeaf77 --- /dev/null +++ b/platform/backend/user_api/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.23 on 2026-03-25 21:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ('website', models.URLField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Companies', + }, + ), + ] diff --git a/platform/backend/user_api/serializers.py b/platform/backend/user_api/serializers.py index de73d72..3b10a57 100644 --- a/platform/backend/user_api/serializers.py +++ b/platform/backend/user_api/serializers.py @@ -32,7 +32,7 @@ def create(self, validated_data): class UserLoginSerializer(serializers.Serializer): email = serializers.EmailField() username = serializers.CharField() - password = serializers.CharField() + password = serializers.CharField(write_only=True) def check_user(self, clean_data): user = authenticate(username=clean_data['username'], password=clean_data['password']) diff --git a/platform/backend/user_api/views.py b/platform/backend/user_api/views.py index 8c73dbf..2f9cfca 100644 --- a/platform/backend/user_api/views.py +++ b/platform/backend/user_api/views.py @@ -12,13 +12,12 @@ class UserRegister(APIView): def post(self, request): # clean_data = custom_validation(request.data) clean_data = request.data - print(clean_data) serializer = UserRegisterSerializer(data=clean_data) - if serializer.is_valid(raise_exception=True): + if serializer.is_valid(): user = serializer.create(clean_data) if user: return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(status=status.HTTP_400_BAD_REQUEST) + return Response({'error': ' '.join([f"{k}: {v[0]}" for k, v in serializer.errors.items()])}, status=status.HTTP_400_BAD_REQUEST) class UserLogin(APIView): @@ -30,10 +29,14 @@ def post(self, request): # assert validate_email(data) # assert validate_password(data) serializer = UserLoginSerializer(data=data) - if serializer.is_valid(raise_exception=True): - user = serializer.check_user(data) + if serializer.is_valid(): + try: + user = serializer.check_user(data) + except KeyError: + return Response({'error': 'Invalid username or password'}, status=status.HTTP_401_UNAUTHORIZED) login(request, user) - return Response(serializer.data, status=status.HTTP_200_OK) # Status nicht immer 200 + return Response(serializer.data, status=status.HTTP_200_OK) + return Response({'error': ' '.join([f"{k}: {v[0]}" for k, v in serializer.errors.items()])}, status=status.HTTP_400_BAD_REQUEST) class UserLogout(APIView): @@ -50,4 +53,6 @@ class UserView(APIView): ## def get(self, request): serializer = UserSerializer(request.user) - return Response({'user': serializer.data}, status=status.HTTP_200_OK) \ No newline at end of file + response = Response({'user': serializer.data}, status=status.HTTP_200_OK) + response['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' + return response \ No newline at end of file diff --git a/platform/frontend/app/competitions/[challengeId]/page.tsx b/platform/frontend/app/competitions/[challengeId]/page.tsx index b38b3a9..aa896b9 100644 --- a/platform/frontend/app/competitions/[challengeId]/page.tsx +++ b/platform/frontend/app/competitions/[challengeId]/page.tsx @@ -22,9 +22,11 @@ import { Badge, Tooltip, Box, + Stack, } from "@mantine/core"; import { ChallengeData, Score, DetailedScores } from "@/app/components/types"; import { useClient, useUser } from "@/app/components/UserContext"; +import Link from "next/link"; import { notFound } from "next/navigation"; import { IconDataset, @@ -69,11 +71,14 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { const [scores, setScores] = useState>([]); const [methodName, setMethodName] = useState(null); const [activeMetric, setActiveMetric] = useState("emd"); + const [isPlotModalOpen, setIsPlotModalOpen] = useState(false); + const [selectedPlot, setSelectedPlot] = useState<{ base64: string, method: string } | null>(null); const { data: scoreData, error: errorLoadingScoreData, isLoading: isLoadingScoreData, + mutate: mutateScores, } = useSWR(`${BASE_URL_API}/api/scores`, fetcher); useEffect(() => { @@ -89,7 +94,7 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { useEffect(() => { client - .get(`/api/challenge/${params.challengeId}`) + .get(`api/challenge/${params.challengeId}`) .then(({ data }) => { setChallenge(convertChallengeData(data)); }) @@ -134,7 +139,7 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { const finalMethodName = methodName?.trim() || defaultMethodName; formData.append("method_name", finalMethodName); client - .post(`/api/xai/${challenge.id}/`, formData) + .post(`api/xai/${challenge.id}`, formData) .then((res) => { const { message, score, detailed_scores }: { message: string; @@ -144,6 +149,7 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { if (score) { setSubmissionUploadScore(convertScore(score)); + mutateScores(); } if (detailed_scores) { setSubmissionUploadDetailedScores(detailed_scores); @@ -247,53 +253,74 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { ) : ( - , s: Score) => { - if (s.username !== user.username) { - return t; - } - return [ - ...t, - [ - formatDateGerman(s.createdAt), - s.methodName || "Unknown Method", - - {formatScore(s.emdScore)} - {s.emdStd && ( - - ±{formatScore(s.emdStd)} - - )} - , - - {formatScore(s.imaScore)} - {s.imaStd && ( - - ±{formatScore(s.imaStd)} - - )} - , - ], - ]; - }, []), - }} - /> + <> +
, s: Score) => { + if (s.username !== user.username) { + return t; + } + return [ + ...t, + [ + formatDateGerman(s.createdAt), + s.methodName || "Unknown Method", + + {formatScore(s.emdScore)} + {s.emdStd && ( + + ±{formatScore(s.emdStd)} + + )} + , + + {formatScore(s.imaScore)} + {s.imaStd && ( + + ±{formatScore(s.imaStd)} + + )} + , + s.plotBase64 ? ( + + ) : ( + "-" + ), + ], + ]; + }, []), + }} + /> + + + + ) ) : ( - You are not logged in... + + You must be logged in to view your submissions and add new ones. + + )} - - - @@ -330,6 +357,27 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { Challenge ID {challenge.id} + {user && user.username === challenge.creator && ( + <> + + + + )} @@ -341,7 +389,7 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { fullWidth variant="light" component="a" - href={`http://localhost:8000/api/dataset/${challenge.id}`} + href={`${BASE_URL_API}/api/dataset/${challenge.id}`} > Download Dataset @@ -351,7 +399,7 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { fullWidth variant="light" component="a" - href={`http://localhost:8000/api/xaimethod/${challenge.id}`} + href={`${BASE_URL_API}/api/xaimethod/${challenge.id}`} > Download XAI Method Template @@ -361,7 +409,7 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { fullWidth variant="light" component="a" - href={`http://localhost:8000/api/mlmodel/${challenge.id}`} + href={`${BASE_URL_API}/api/mlmodel/${challenge.id}`} > Download ML Model @@ -385,7 +433,7 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { - Earth Mover's Distance - measures spatial alignment + {"Earth Mover's Distance - measures spatial alignment"} @@ -429,6 +477,9 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { + @@ -466,6 +517,22 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { )} + ))} @@ -541,6 +608,22 @@ const ChallengeDetail = ({ params }: { params: { challengeId: string } }) => { )} + + { + setIsPlotModalOpen(false); + setSelectedPlot(null); + }} + title={selectedPlot ? `${selectedPlot.method} on ${challenge?.title || "Challenge"}` : "XAI Heatmaps"} + size="auto" + > + {selectedPlot && ( +
+ +
+ )} +
); }; diff --git a/platform/frontend/app/competitions/create/page.tsx b/platform/frontend/app/competitions/create/page.tsx index 1db4129..c18d524 100644 --- a/platform/frontend/app/competitions/create/page.tsx +++ b/platform/frontend/app/competitions/create/page.tsx @@ -22,11 +22,18 @@ const CreateChallenge = () => { const user = useUser(); const router = useRouter(); + React.useEffect(() => { + if (user === null) { + router.push("/login"); + } + }, [user, router]); + const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [dataset, setDataset] = useState(null); const [mlmodel, setMlmodel] = useState(null); const [xaiMethod, setXaiMethod] = useState(null); + const [thumbnail, setThumbnail] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -34,6 +41,7 @@ const CreateChallenge = () => { const datasetInputRef = useRef(null); const mlmodelInputRef = useRef(null); const xaiMethodInputRef = useRef(null); + const thumbnailInputRef = useRef(null); // Custom file change handler const handleFileChange = (event: ChangeEvent, setter: React.Dispatch>) => { @@ -77,13 +85,16 @@ const CreateChallenge = () => { formData.append("dataset", dataset); formData.append("mlmodel", mlmodel); formData.append("xai_method", xaiMethod); + if (thumbnail) { + formData.append("thumbnail", thumbnail); + } // Add creator if user is authenticated if (user && user.username) { formData.append("creator", user.username); } - const response = await client.post("/api/challenge/create/", formData, { + const response = await client.post("api/challenge/create", formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -99,7 +110,6 @@ const CreateChallenge = () => { } }; - // Custom file input component const CustomFileInput = ({ label, description, @@ -107,7 +117,8 @@ const CreateChallenge = () => { value, onChange, onClear, - accept = "*/*" + accept = "*/*", + required = true }: { label: string; description: string; @@ -116,11 +127,12 @@ const CreateChallenge = () => { onChange: (e: ChangeEvent) => void; onClear: () => void; accept?: string; + required?: boolean; }) => { return ( - {label} * + {label} {required && *} {description} @@ -234,12 +246,22 @@ const CreateChallenge = () => { accept=".py,.ipynb,.txt,text/plain,text/python,application/x-python" /> + handleFileChange(e, setThumbnail)} + onClear={() => clearFileInput(thumbnailInputRef, setThumbnail)} + accept="image/png, image/jpeg, image/jpg" + required={false} + /> + - + diff --git a/platform/frontend/app/components/FileUpload 2.tsx b/platform/frontend/app/components/FileUpload 2.tsx index 806b923..5da47c9 100644 --- a/platform/frontend/app/components/FileUpload 2.tsx +++ b/platform/frontend/app/components/FileUpload 2.tsx @@ -5,6 +5,7 @@ import CircularProgress from "@mui/material/CircularProgress"; import { Dropzone } from '@mantine/dropzone'; import { Text, Group } from '@mantine/core'; import { IconUpload, IconX, IconFile } from '@tabler/icons-react'; +import { BASE_URL_API } from "./utils"; export const FileUpload = () => { const [uploadStatus, setUploadStatus] = useState(""); @@ -22,7 +23,7 @@ export const FileUpload = () => { setIsLoading(true); try { - await axios.post("http://localhost:8000/api/xai/f85f311b-9997-429b-9b08-5397140174ed/", formData, {}); + await axios.post(`${BASE_URL_API}/api/xai/f85f311b-9997-429b-9b08-5397140174ed/`, formData, {}); setUploadStatus(`✅ File uploaded successfully`); setIsUploadSuccessful(true); setScore(null); @@ -40,7 +41,7 @@ export const FileUpload = () => { const handleGetScoreClick = async () => { try { - const response = await axios.get("http://localhost:8000/api/score/1/"); + const response = await axios.get(`${BASE_URL_API}/api/score/1/`); setScore(`Score: ${response.data.score}`); setError(null); } catch (error) { diff --git a/platform/frontend/app/components/Header.tsx b/platform/frontend/app/components/Header.tsx index cb7ae08..fe3ef12 100644 --- a/platform/frontend/app/components/Header.tsx +++ b/platform/frontend/app/components/Header.tsx @@ -43,7 +43,7 @@ const Header = () => { }; const handleLogout = () => { - client.post("/logout", user).then(() => { + client.post("logout", user).then(() => { updateUser(null); clearSession(); }).catch((error) => { @@ -58,24 +58,24 @@ const Header = () => { } return ( -
+
{smallScreen ? ( - - evalXAI + + EXACT ) : ( <> - - evalXAI: Explainable AI Benchmarking Platform{" "} - + + EXACT: Explainable AI Comparison Toolkit{" "} + hosted by diff --git a/platform/frontend/app/components/SingleCompetition.tsx b/platform/frontend/app/components/SingleCompetition.tsx index a1dfb42..858b153 100644 --- a/platform/frontend/app/components/SingleCompetition.tsx +++ b/platform/frontend/app/components/SingleCompetition.tsx @@ -6,6 +6,7 @@ import { Button } from "@mui/material"; import axios from "axios"; import TetrisImage from "../../public/TetrisImage.png"; import Image from "next/image"; +import { BASE_URL_API } from "./utils"; interface SingleCompetitionProps { competitionName: string; } @@ -15,7 +16,7 @@ export const SingleCompetition = (props: SingleCompetitionProps) => { const challengeId = 1; try { const response = await axios.get( - `localhost:8000/api/dataset/${challengeId}/`, + `${BASE_URL_API}/api/dataset/${challengeId}/`, { responseType: "blob" } ); const url = window.URL.createObjectURL(new Blob([response.data])); @@ -54,7 +55,7 @@ export const SingleCompetition = (props: SingleCompetitionProps) => { stored in a database.

- + console.log(file)} /> diff --git a/platform/frontend/app/components/SubmissionUpload.tsx b/platform/frontend/app/components/SubmissionUpload.tsx index bc31daa..74878f2 100644 --- a/platform/frontend/app/components/SubmissionUpload.tsx +++ b/platform/frontend/app/components/SubmissionUpload.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { Button } from '@mantine/core'; interface SubmissionUploadProps { onFileSelect: (file: File) => void; @@ -115,8 +116,10 @@ const SubmissionUpload: React.FC = ({ > {file.name} - + ))}
- +
)} @@ -152,7 +156,7 @@ const SubmissionUpload: React.FC = ({ )} {error && ( -
+
{error}
)} diff --git a/platform/frontend/app/components/UserContext.tsx b/platform/frontend/app/components/UserContext.tsx index d91fae6..6f3085a 100644 --- a/platform/frontend/app/components/UserContext.tsx +++ b/platform/frontend/app/components/UserContext.tsx @@ -3,6 +3,7 @@ import React, { useState, useContext, useEffect } from "react"; import { UserData } from "./types"; import axios, { AxiosInstance } from "axios"; import { BASE_URL_API } from "./utils"; +import { Storage } from "../utils/storage"; axios.defaults.xsrfCookieName = "csrftoken"; axios.defaults.xsrfHeaderName = "X-CSRFToken"; @@ -36,12 +37,14 @@ export function UserProvider({ children }: { children: React.ReactNode }) { useEffect(() => { client - .get("/user") + .get("user", { headers: { 'Cache-Control': 'no-cache' } }) .then(({ data }) => { setUser(data.user); }) .catch(() => { setUser(null); + Storage.removeLocalStorage('user'); + Storage.removeCookie('sessionActive'); }); }, []); diff --git a/platform/frontend/app/components/types.ts b/platform/frontend/app/components/types.ts index a82a3ab..a7401c7 100644 --- a/platform/frontend/app/components/types.ts +++ b/platform/frontend/app/components/types.ts @@ -38,6 +38,7 @@ export interface Score { emdStd?: number | null; imaScore?: number | null; imaStd?: number | null; + plotBase64?: string | null; status?: string; } diff --git a/platform/frontend/app/components/utils.ts b/platform/frontend/app/components/utils.ts index 8b6bad3..4e54245 100644 --- a/platform/frontend/app/components/utils.ts +++ b/platform/frontend/app/components/utils.ts @@ -6,11 +6,11 @@ import { IconFileSpreadsheet, } from "@tabler/icons-react"; -export const BASE_URL_API = "http://localhost:8000"; +export const BASE_URL_API = typeof window !== 'undefined' ? '/backend' : "http://backend:8000"; -export const NO_HEADER_PAGES = ["/login", "/register"]; // pages where the header should be hidden +export const NO_HEADER_PAGES: string[] = []; // pages where the header should be hidden -export const NO_FOOTER_PAGES = ["/login", "/register"]; // pages where the footer should be hidden +export const NO_FOOTER_PAGES: string[] = []; // pages where the footer should be hidden export const AUTHENTICATION_OPTIONS: Array = [ { name: "Email", icon: IconMail }, @@ -23,8 +23,8 @@ export const convertChallengeData = (c: any) => { deadline: null, createdAt: new Date(c.created_at), thumbnail: - c.thumbnail || - "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Typical_Tetris_Game.svg/1200px-Typical_Tetris_Game.svg.png", + c?.thumbnail || + "/TetrisImage.png", creator: c.creator || null, participants: c.participants || null, }; @@ -44,7 +44,7 @@ export const CHALLENGES_MOCK_DATA: Array = [ description: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", thumbnail: - "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Typical_Tetris_Game.svg/1200px-Typical_Tetris_Game.svg.png", + "/TetrisImage.png", participants: 13, creator: "Rick", createdAt: new Date(new Date().getTime() - 5 * 24 * 60 * 60 * 1000), @@ -59,7 +59,7 @@ export const CHALLENGES_MOCK_DATA: Array = [ description: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", thumbnail: - "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Typical_Tetris_Game.svg/1200px-Typical_Tetris_Game.svg.png", + "/TetrisImage.png", participants: 4, creator: "Rick", createdAt: new Date(new Date().getTime() - 10 * 24 * 60 * 60 * 1000), @@ -74,7 +74,7 @@ export const CHALLENGES_MOCK_DATA: Array = [ description: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", thumbnail: - "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Typical_Tetris_Game.svg/1200px-Typical_Tetris_Game.svg.png", + "/TetrisImage.png", participants: 0, creator: "Benny", createdAt: new Date(new Date().getTime() - 19 * 24 * 60 * 60 * 1000), @@ -89,7 +89,7 @@ export const CHALLENGES_MOCK_DATA: Array = [ description: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", thumbnail: - "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Typical_Tetris_Game.svg/1200px-Typical_Tetris_Game.svg.png", + "/TetrisImage.png", participants: 49, creator: "Rick", createdAt: new Date(new Date().getTime() - 2 * 24 * 60 * 60 * 1000), @@ -104,7 +104,7 @@ export const CHALLENGES_MOCK_DATA: Array = [ description: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", thumbnail: - "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Typical_Tetris_Game.svg/1200px-Typical_Tetris_Game.svg.png", + "/TetrisImage.png", participants: 1, creator: "Rick", createdAt: new Date(new Date().getTime() - 4 * 24 * 60 * 60 * 1000), @@ -139,6 +139,7 @@ export const convertScore = (score: any) => { emdStd: score["emd_std"] || null, imaScore: score["ima_score"] || null, imaStd: score["ima_std"] || null, + plotBase64: score["plot_base64"] || null, status: score["status"] || "completed", }; }; diff --git a/platform/frontend/app/datasets/page.tsx b/platform/frontend/app/datasets/page.tsx index 92c3837..be64477 100644 --- a/platform/frontend/app/datasets/page.tsx +++ b/platform/frontend/app/datasets/page.tsx @@ -7,7 +7,7 @@ import { Typography, } from "@mui/material"; import Link from "next/link"; -import tetris from "@/public/tetris.png"; +import tetris from "@/public/TetrisImage.png"; const Datasets = () => { const competitionName = "Tetris"; diff --git a/platform/frontend/app/login/page.tsx b/platform/frontend/app/login/page.tsx index 79506f7..0dba2fa 100644 --- a/platform/frontend/app/login/page.tsx +++ b/platform/frontend/app/login/page.tsx @@ -15,7 +15,7 @@ import { } from "@mantine/core"; import { IconArrowLeft, IconExclamationCircle } from "@tabler/icons-react"; import Link from "next/link"; -import logo from "../components/evalXAI_logo.png"; +import logo from "@/public/logo_ptb.png"; import NextImage from "next/image"; import { AUTHENTICATION_OPTIONS } from "../components/utils"; import { AuthenticationOption } from "../components/types"; @@ -43,7 +43,7 @@ const Login = () => { const handleLogin = () => { setIsLoadingLogin(true); client - .post(`/login`, { + .post(`login`, { username, email: `${username}@mail.de`, password, @@ -59,8 +59,9 @@ const Login = () => { .catch((e) => { console.error('Login error:', e); setIsLoadingLogin(false); + const errorMessage = e.response?.data?.error || e.message; setAuthenticationError( - "The username or password provided is incorrect." + `Login failed: ${errorMessage || "The server is currently unavailable."}` ); }); }; diff --git a/platform/frontend/app/page.tsx b/platform/frontend/app/page.tsx index dcf5831..f2921c9 100644 --- a/platform/frontend/app/page.tsx +++ b/platform/frontend/app/page.tsx @@ -1,18 +1,21 @@ "use client"; import React from "react"; import Link from "next/link"; -import { - Button, - Paper, - Text, - Title, +import { + Button, + Paper, + Text, + Title, Container, Group, Stack, Badge, ThemeIcon, - SimpleGrid + SimpleGrid, + Image } from "@mantine/core"; +import logo_ptb from "@/public/logo_ptb.png"; +import NextImage from "next/image"; import { IconAward, IconSelect, @@ -29,7 +32,7 @@ const steps = [ icon: , }, { - step: "Browse Competitions", + step: "Browse Competitions", description: "Select from available XAI benchmarking challenges", icon: , }, @@ -56,23 +59,33 @@ export default function Home() { {/* Hero Section */} + {/* */}
- Welcome to{" "} <Text fw={900} - variant="gradient" + c="ptbBlue.5" inherit component="span" - gradient={{ from: "blue", to: "cyan", deg: 90 }} > - evalXAI - </Text> + EXACT + </Text> + + + Explainable AI Comparison Toolkit Explore and participate in Explainable AI Benchmarking Challenges. @@ -95,14 +108,13 @@ export default function Home() { {s.icon} - @@ -130,8 +142,7 @@ export default function Home() {
{activeMetric === "ima" ? "IMA" : "EMD"} + Heatmaps +
+ {s.plotBase64 ? ( + + ) : ( + "-" + )} +