From 16ce59ae980eb3dd554ece0d87476594f55c3fac Mon Sep 17 00:00:00 2001 From: Josako Date: Thu, 25 Sep 2025 17:28:01 +0200 Subject: [PATCH] - Introduce cache busting (to circumvent aggressive caching on iOS - but ideal in other contexts as well) - Change the build process to allow cache busting - Optimisations to the build process - Several improvements of UI geared towards mobile experience - --- .gitignore | 1 + common/utils/asset_manifest.py | 45 + common/utils/template_filters.py | 28 + config/static-manifest/manifest.json | 6 + docker/build_and_push_eveai.sh | 201 ++- docker/eveai_chat_client/Dockerfile | 1 - docker/nginx/Dockerfile | 5 +- docker/rebuild_chat_client.sh | 10 +- documentation/cache-busting-and-builds.md | 106 ++ documentation/migratie_startup_en_builds.md | 208 +++ documentation/mobile-ux.md | 108 ++ eveai_app/templates/head.html | 2 +- eveai_app/templates/scripts.html | 2 +- .../static/assets/css/chat-components.css | 42 +- .../static/assets/css/chat-input.css | 7 - .../static/assets/css/chat-message.css | 5 - eveai_chat_client/static/assets/css/chat.css | 25 +- eveai_chat_client/static/assets/css/form.css | 4 - .../assets/vue-components/ChatInput.vue | 12 +- .../assets/vue-components/ChatMessage.vue | 18 +- .../assets/vue-components/DynamicForm.vue | 10 +- .../assets/vue-components/FormMessage.vue | 2 +- .../assets/vue-components/MobileHeader.vue | 20 +- eveai_chat_client/templates/base.html | 7 +- eveai_chat_client/templates/scripts.html | 2 +- frontend_src/js/chat-client.js | 97 -- nginx/frontend_src/entries/chat-client.html | 12 + nginx/frontend_src/entries/main.html | 12 + nginx/nginx.conf | 6 + nginx/package-lock.json | 1357 +++++++++-------- nginx/package.json | 21 +- nginx/scripts/generate-manifest.mjs | 55 + 32 files changed, 1538 insertions(+), 899 deletions(-) create mode 100644 common/utils/asset_manifest.py create mode 100644 config/static-manifest/manifest.json create mode 100644 documentation/cache-busting-and-builds.md create mode 100644 documentation/migratie_startup_en_builds.md create mode 100644 documentation/mobile-ux.md delete mode 100644 frontend_src/js/chat-client.js create mode 100644 nginx/frontend_src/entries/chat-client.html create mode 100644 nginx/frontend_src/entries/main.html create mode 100644 nginx/scripts/generate-manifest.mjs diff --git a/.gitignore b/.gitignore index ead2572..7406241 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc /nginx/node_modules/ /nginx/.parcel-cache/ /nginx/static/ +/docker/build_logs/ diff --git a/common/utils/asset_manifest.py b/common/utils/asset_manifest.py new file mode 100644 index 0000000..bf47f9f --- /dev/null +++ b/common/utils/asset_manifest.py @@ -0,0 +1,45 @@ +import json +import os +from functools import lru_cache +from typing import Dict + +# Default manifest path inside app images; override with env +DEFAULT_MANIFEST_PATH = os.environ.get( + 'EVEAI_STATIC_MANIFEST_PATH', + '/app/config/static-manifest/manifest.json' +) + + +@lru_cache(maxsize=1) +def _load_manifest(manifest_path: str = DEFAULT_MANIFEST_PATH) -> Dict[str, str]: + try: + with open(manifest_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception: + return {} + + +def resolve_asset(logical_path: str, manifest_path: str = DEFAULT_MANIFEST_PATH) -> str: + """ + Map a logical asset path (e.g. 'dist/chat-client.js') to the hashed path + found in the Parcel manifest. If not found or manifest missing, return the + original logical path for graceful fallback. + """ + if not logical_path: + return logical_path + + manifest = _load_manifest(manifest_path) + + # Try several key variants as Parcel manifests may use different keys + candidates = [ + logical_path, + logical_path.lstrip('/'), + logical_path.replace('static/', ''), + logical_path.replace('dist/', ''), + ] + + for key in candidates: + if key in manifest: + return manifest[key] + + return logical_path diff --git a/common/utils/template_filters.py b/common/utils/template_filters.py index bbc4e8a..8eff017 100644 --- a/common/utils/template_filters.py +++ b/common/utils/template_filters.py @@ -107,6 +107,33 @@ def get_pagination_html(pagination, endpoint, **kwargs): return Markup(''.join(html)) +def asset_url(logical_path: str): + """ + Resolve an asset logical path to a hashed URL using Parcel manifest when available. + Fallback to the original logical path under /static/ if manifest is missing. + Examples: + - asset_url('dist/chat-client.js') -> '/static/dist/chat-client.abc123.js' + - asset_url('dist/chat-client.css') -> '/static/dist/chat-client.def456.css' + """ + if not logical_path: + return logical_path + try: + from common.utils.asset_manifest import resolve_asset + resolved = resolve_asset(logical_path) + if not resolved: + return f"/static/{logical_path.lstrip('/')}" + # If resolved is already an absolute URL starting with /static or http(s), return as is + if resolved.startswith('/static/') or resolved.startswith('http://') or resolved.startswith('https://'): + return resolved + # If it starts with 'dist/', prefix /static/ + if resolved.startswith('dist/'): + return '/static/' + resolved + # Otherwise, best effort: ensure it lives under /static/ + return '/static/' + resolved.lstrip('/') + except Exception: + return f"/static/{logical_path.lstrip('/')}" + + def register_filters(app): """ Registers custom filters with the Flask app. @@ -123,4 +150,5 @@ def register_filters(app): app.jinja_env.globals['prefixed_url_for'] = prefixed_url_for app.jinja_env.globals['get_pagination_html'] = get_pagination_html app.jinja_env.globals['get_base_background_color'] = get_base_background_color + app.jinja_env.globals['asset_url'] = asset_url diff --git a/config/static-manifest/manifest.json b/config/static-manifest/manifest.json new file mode 100644 index 0000000..a28e9ca --- /dev/null +++ b/config/static-manifest/manifest.json @@ -0,0 +1,6 @@ +{ + "dist/chat-client.js": "dist/chat-client.25888758.js", + "dist/chat-client.css": "dist/chat-client.eef0ef31.css", + "dist/main.js": "dist/main.f3dde0f6.js", + "dist/main.css": "dist/main.c40e57ad.css" +} \ No newline at end of file diff --git a/docker/build_and_push_eveai.sh b/docker/build_and_push_eveai.sh index e0c708c..41a511a 100755 --- a/docker/build_and_push_eveai.sh +++ b/docker/build_and_push_eveai.sh @@ -1,7 +1,49 @@ #!/bin/bash -# Exit on any error -set -e +# Safer bash: we manage errors manually (no -e) but detect pipeline failures +set -o pipefail + +# Quiet mode default; enable verbose with --verbose +QUIET=${QUIET:-true} + +# Parse --verbose early (we'll reparse fully later as well) +for arg in "$@"; do + if [[ "$arg" == "--verbose" ]]; then QUIET=false; fi +done + +# Per-run logs directory +RUN_TS=$(date +%Y%m%d_%H%M%S) +LOG_DIR="./build_logs/$RUN_TS" +mkdir -p "$LOG_DIR" + +# Error aggregation +ERRORS=() +ERROR_LINES=() +EXIT_CODE=0 + +# Helper: run_quiet SERVICE STEP -- CMD ARGS... +run_quiet() { + local SERVICE="$1"; shift + local STEP="$1"; shift + # Expect a literal "--" separator before the command + if [[ "$1" == "--" ]]; then shift; fi + local LOG_FILE="$LOG_DIR/${SERVICE}.${STEP}.log" + if [[ "$QUIET" == "true" ]]; then + "$@" > /dev/null 2> >(tee -a "$LOG_FILE" >&2) + else + "$@" > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2) + fi + local RC=$? + echo "$LOG_FILE" > "$LOG_DIR/.last_${SERVICE}_${STEP}.path" + return $RC +} + +record_error() { + local SERVICE="$1"; local STEP="$2"; local MESSAGE="$3"; local LOG_FILE="$4" + ERRORS+=("$SERVICE|$STEP|$LOG_FILE|$MESSAGE") + ERROR_LINES+=("$MESSAGE") + EXIT_CODE=1 +} source ./podman_env_switch.sh dev @@ -39,7 +81,7 @@ BASE_ONLY="" # Function to display usage information usage() { - echo "Usage: $0 [-b|-p|-bb|--base-only] [--no-cache] [--progress=plain] [--debug] [service1 service2 ...]" + echo "Usage: $0 [-b|-p|-bb|--base-only] [--no-cache] [--progress=plain] [--debug] [--verbose] [service1 service2 ...]" echo " -b: Build only" echo " -p: Push only" echo " -bb: Build base image (in addition to services)" @@ -47,6 +89,7 @@ usage() { echo " --no-cache: Perform a clean build without using cache" echo " --progress=plain: Show detailed progress of the build" echo " --debug: Enable debug mode for the build" + echo " --verbose: Show full output of build/push (default is quiet; logs always saved under ./build_logs/)" echo " If no option is provided, both build and push will be performed." echo " If no services are specified, all eveai_ services and nginx will be processed." echo " All images are built for AMD64 platform (compatible with both x86_64 and Apple Silicon via emulation)." @@ -55,6 +98,10 @@ usage() { # Parse command-line options while [[ $# -gt 0 ]]; do case $1 in + --verbose) + QUIET=false + shift + ;; -b) ACTION="build" shift @@ -96,13 +143,13 @@ done # Function to build base image build_base_image() { - echo "πŸ—οΈ Building base image..." + echo "πŸ—οΈ Building base image... ==============================================================" local BASE_IMAGE_NAME="$REGISTRY/$ACCOUNT/eveai-base:$TAG" echo "Building base image for platform: $PLATFORM" echo "Base image tag: $BASE_IMAGE_NAME" - podman build \ + run_quiet base build -- podman build \ --platform "$PLATFORM" \ $NO_CACHE \ $PROGRESS \ @@ -111,12 +158,24 @@ build_base_image() { -t "$BASE_IMAGE_NAME" \ -f Dockerfile.base \ .. - + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/base.build.log" + echo "❌ Failed to build base image" + record_error base build "❌ Failed to build base image" "$LOG_FILE" + return 1 + fi + if [ "$ACTION" = "push" ] || [ "$ACTION" = "both" ]; then echo "Pushing base image to registry..." - podman push "$BASE_IMAGE_NAME" + run_quiet base push -- podman push "$BASE_IMAGE_NAME" + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/base.push.log" + echo "❌ Failed to push base image" + record_error base push "❌ Failed to push base image" "$LOG_FILE" + return 1 + fi fi - + echo "βœ… Base image built successfully" } @@ -132,7 +191,7 @@ should_build_base() { # Function to build and/or push a service process_service() { local SERVICE="$1" - echo "Processing $SERVICE..." + echo "Processing $SERVICE... ==================================================================" # Extract the build context and dockerfile from the compose file CONTEXT=$(yq e ".services.$SERVICE.build.context" compose_dev.yaml) @@ -160,8 +219,8 @@ process_service() { # Build and/or push based on ACTION if [ "$ACTION" = "build" ]; then - echo "Building $SERVICE for $PLATFORM..." - podman build \ + echo "πŸ› οΈ Building $SERVICE for $PLATFORM..." + run_quiet "$SERVICE" build -- podman build \ --platform "$PLATFORM" \ $NO_CACHE \ $PROGRESS \ @@ -170,26 +229,27 @@ process_service() { -t "$REGISTRY_IMAGE_NAME" \ -f "$CONTEXT/$DOCKERFILE" \ "$CONTEXT" + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/${SERVICE}.build.log" + echo "❌ Failed to build $SERVICE" + record_error "$SERVICE" build "❌ Failed to build $SERVICE" "$LOG_FILE" + return 1 + fi elif [ "$ACTION" = "push" ]; then - echo "Building and pushing $SERVICE for $PLATFORM..." - podman build \ - --platform "$PLATFORM" \ - $NO_CACHE \ - $PROGRESS \ - $DEBUG \ - -t "$LOCAL_IMAGE_NAME" \ - -t "$REGISTRY_IMAGE_NAME" \ - -f "$CONTEXT/$DOCKERFILE" \ - "$CONTEXT" - - echo "Pushing $SERVICE to registry..." - podman push "$REGISTRY_IMAGE_NAME" + echo "πŸ“€ Pushing $SERVICE to registry..." + run_quiet "$SERVICE" push -- podman push "$REGISTRY_IMAGE_NAME" + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/${SERVICE}.push.log" + echo "❌ Failed to push $SERVICE" + record_error "$SERVICE" push "❌ Failed to push $SERVICE" "$LOG_FILE" + return 1 + fi else # Both build and push - echo "Building $SERVICE for $PLATFORM..." - podman build \ + echo "πŸ› οΈ Building $SERVICE for $PLATFORM..." + run_quiet "$SERVICE" build -- podman build \ --platform "$PLATFORM" \ $NO_CACHE \ $PROGRESS \ @@ -198,9 +258,21 @@ process_service() { -t "$REGISTRY_IMAGE_NAME" \ -f "$CONTEXT/$DOCKERFILE" \ "$CONTEXT" + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/${SERVICE}.build.log" + echo "❌ Failed to build $SERVICE" + record_error "$SERVICE" build "❌ Failed to build $SERVICE" "$LOG_FILE" + return 1 + fi - echo "Pushing $SERVICE to registry..." - podman push "$REGISTRY_IMAGE_NAME" + echo "πŸ“€ Pushing $SERVICE to registry..." + run_quiet "$SERVICE" push -- podman push "$REGISTRY_IMAGE_NAME" + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/${SERVICE}.push.log" + echo "❌ Failed to push $SERVICE" + record_error "$SERVICE" push "❌ Failed to push $SERVICE" "$LOG_FILE" + return 1 + fi fi } @@ -231,22 +303,87 @@ fi echo "Using simplified AMD64-only approach for maximum compatibility..." echo "Images will be tagged as: $REGISTRY/$ACCOUNT/[service]:$TAG" +# Reorder to ensure nginx builds before eveai_* if both are present +HAS_NGINX=false +HAS_APPS=false +for S in "${SERVICES[@]}"; do + if [[ "$S" == "nginx" ]]; then HAS_NGINX=true; fi + if [[ "$S" == eveai_* ]]; then HAS_APPS=true; fi +done + +if $HAS_NGINX && $HAS_APPS; then + ORDERED_SERVICES=("nginx") + for S in "${SERVICES[@]}"; do + if [[ "$S" != "nginx" ]]; then ORDERED_SERVICES+=("$S"); fi + done + SERVICES=("${ORDERED_SERVICES[@]}") +fi + # Loop through services for SERVICE in "${SERVICES[@]}"; do if [[ "$SERVICE" == "nginx" ]]; then - ./copy_specialist_svgs.sh ../config ../nginx/static/assets 2>/dev/null || echo "Warning: copy_specialist_svgs.sh not found or failed" + run_quiet nginx copy-specialist-svgs -- ./copy_specialist_svgs.sh ../config ../nginx/static/assets + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/nginx.copy-specialist-svgs.log" + echo "⚠️ copy_specialist_svgs.sh not found or failed" + record_error nginx copy-specialist-svgs "⚠️ copy_specialist_svgs.sh not found or failed" "$LOG_FILE" + fi + run_quiet nginx rebuild-chat-client -- ./rebuild_chat_client.sh + if [ $? -ne 0 ]; then + LOG_FILE="$LOG_DIR/nginx.rebuild-chat-client.log" + echo "❌ rebuild_chat_client.sh failed" + record_error nginx rebuild-chat-client "❌ rebuild_chat_client.sh failed" "$LOG_FILE" + fi + MANIFEST_SRC="../nginx/static/dist/manifest.json" + MANIFEST_DST_DIR="../config/static-manifest" + MANIFEST_DST="$MANIFEST_DST_DIR/manifest.json" + if [ ! -f "$MANIFEST_SRC" ]; then + if $HAS_NGINX; then + echo "⚠️ manifest.json not found at $MANIFEST_SRC yet. nginx should be built first in this run." + else + echo "❌ manifest.json not found at $MANIFEST_SRC. Please build nginx (assets) first." + exit 1 + fi + fi + mkdir -p "$MANIFEST_DST_DIR" + if [ -f "$MANIFEST_SRC" ]; then + cp -f "$MANIFEST_SRC" "$MANIFEST_DST" + echo "πŸ“„ Staged manifest at $MANIFEST_DST" + fi fi + if [[ "$SERVICE" == "nginx" || "$SERVICE" == eveai_* || "$SERVICE" == "prometheus" || "$SERVICE" == "grafana" ]]; then if process_service "$SERVICE"; then echo "βœ… Successfully processed $SERVICE" else echo "❌ Failed to process $SERVICE" + ERROR_LINES+=("❌ Failed to process $SERVICE") + EXIT_CODE=1 fi else echo "⏭️ Skipping $SERVICE as it's not nginx, prometheus, grafana or doesn't start with eveai_" fi done -echo -e "\033[32mβœ… All specified services processed successfully!\033[0m" -echo -e "\033[32mπŸ“¦ Images are available locally and in registry\033[0m" -echo -e "\033[32mπŸ• Finished at $(date +"%d/%m/%Y %H:%M:%S")\033[0m" \ No newline at end of file +if [ ${#ERRORS[@]} -eq 0 ]; then + echo -e "\033[32mβœ… All specified services processed successfully!\033[0m" + echo -e "\033[32mπŸ“¦ Images are available locally and in registry\033[0m" +else + echo -e "\033[31m❌ One or more errors occurred during build/push\033[0m" + # Reprint short failure lines (your concise messages) + for LINE in "${ERROR_LINES[@]}"; do + echo "$LINE" + done + echo "" + echo "Details (see logs for full output):" + for ITEM in "${ERRORS[@]}"; do + SERVICE_STEP_MSG_LOG=$(echo "$ITEM") + IFS='|' read -r SVC STEP LOGFILE MSG <<< "$SERVICE_STEP_MSG_LOG" + echo "- Service: $SVC | Step: $STEP" + echo " ↳ Log: $LOGFILE" + done + EXIT_CODE=1 +fi +# Always print finished timestamp +echo -e "\033[32mπŸ• Finished at $(date +"%d/%m/%Y %H:%M:%S")\033[0m" +exit $EXIT_CODE \ No newline at end of file diff --git a/docker/eveai_chat_client/Dockerfile b/docker/eveai_chat_client/Dockerfile index 8e15da0..c7616e6 100644 --- a/docker/eveai_chat_client/Dockerfile +++ b/docker/eveai_chat_client/Dockerfile @@ -3,4 +3,3 @@ FROM registry.ask-eve-ai-local.com/josakola/eveai-base:latest # Copy the service-specific source code into the container. COPY eveai_chat_client /app/eveai_chat_client COPY content /app/content - diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile index 310c7c5..bacef45 100644 --- a/docker/nginx/Dockerfile +++ b/docker/nginx/Dockerfile @@ -1,6 +1,9 @@ # Use the official Nginx image as the base image -FROM nginx:latest +ARG TARGETPLATFORM +FROM --platform=$TARGETPLATFORM nginx:latest +# Ensure we use user root +USER root # Copy the custom Nginx configuration file into the container COPY ../../nginx/nginx.conf /etc/nginx/nginx.conf diff --git a/docker/rebuild_chat_client.sh b/docker/rebuild_chat_client.sh index e58654a..3c48323 100755 --- a/docker/rebuild_chat_client.sh +++ b/docker/rebuild_chat_client.sh @@ -1,20 +1,12 @@ #!/bin/bash -cd /Volumes/OWC4M2_1/Development/Josako/EveAI/TBD/docker -source ./docker_env_switch.sh dev - echo "Copying client images" cp -fv ../eveai_chat_client/static/assets/img/* ../nginx/static/assets/img - -pcdown - cd ../nginx npm run clean npm run build -cd ../docker -./build_and_push_eveai.sh -b nginx +cd ../docker/ -pcup diff --git a/documentation/cache-busting-and-builds.md b/documentation/cache-busting-and-builds.md new file mode 100644 index 0000000..1f1b578 --- /dev/null +++ b/documentation/cache-busting-and-builds.md @@ -0,0 +1,106 @@ +# Cache busting, builds en distributie (Parcel v2 + manifest) + +Dit document beschrijft hoe cache busting nu structureel is geΓ―mplementeerd met Parcel v2 (filename hashing) en hoe je builds en distributie uitvoert in de verschillende omgevingen. + +## Overzicht van de aanpak + +- Parcel v2 bouwt de frontend bundles met content-hashes in de bestandsnamen (bijv. `/static/dist/chat-client.abcd123.js`). +- We bouwen via minimale HTML entries (frontend_src/entries/*.html) die de JS importeren; dit dwingt Parcel tot consistente hashing en CSS-extractie. +- Na de build schrijven we een `manifest.json` in `nginx/static/dist/` met een mapping van logische namen naar gehashte paden. +- Templates gebruiken nu `asset_url('dist/chat-client.js|css')` om automatisch naar de juiste gehashte bestanden te verwijzen. +- Nginx (DEV/TEST) is ingesteld om voor `/static/dist/` lange cache headers te sturen: `Cache-Control: public, max-age=31536000, immutable`. +- HTML krijgt geen agressieve caching via Nginx; de browser ontdekt bij volgende request de nieuwe gehashte asset-URLs. +- In STAGING/PROD (Ingress + Bunny) volg je dezelfde principes; CDN ziet unieke URLs en cachet langdurig, zonder purges te vereisen. + +## Relevante bestanden die zijn aangepast/toegevoegd + +- `nginx/package.json` + - DevDependency: `@parcel/reporter-bundle-manifest` toegevoegd. + - `postbuild` script toegevoegd (optioneel; puur informatief). +- `nginx/.parcelrc` + - Parcel reporter geconfigureerd zodat `manifest.json` wordt weggeschreven. +- `common/utils/template_filters.py` + - Nieuwe Jinja global `asset_url(logical_path)` geregistreerd. +- `eveai_chat_client/asset_manifest.py` + - Hulpfuncties om manifest in te lezen en paden te resolven met caching. +- `eveai_chat_client/templates/base.html` en `templates/scripts.html` + - CSS/JS referenties omgezet naar `{{ asset_url('dist/chat-client.css|js') }}`. +- `nginx/nginx.conf` + - Extra location voor `/static/dist/` met lange cache headers. + +## Build uitvoeren (lokaal of in CI) + +1. Ga naar de `nginx` directory. +2. Installeer dependencies (eenmalig of na wijzigingen): + - `npm install` +3. Run build: + - `npm run clean` + - `npm run build` + +Resultaat: +- Bundles met content-hash in `nginx/static/dist/` +- `nginx/static/dist/manifest.json` aanwezig + +Tip: De build wordt ook aangeroepen door het bestaande script `docker/rebuild_chat_client.sh`. + +## rebuild_chat_client.sh + +Script: `docker/rebuild_chat_client.sh` +- Kopieert client images naar `nginx/static/assets/img`. +- Voert vervolgens `npm run clean` en `npm run build` uit in `nginx/`. +- Bouwt en pusht daarna de `nginx` container. + +Door de nieuwe Parcel configuratie zal de build automatisch `manifest.json` genereren en gehashte assets outputten. Er zijn geen extra stappen nodig in dit script. + +## Hoe de templates de juiste bestanden vinden + +- De Jinja global `asset_url()` zoekt in `manifest.json` de gehashte bestandsnaam op basis van een logische naam. +- Voorbeelden in templates: + - CSS: `{{ asset_url('dist/chat-client.css') }}` + - JS: `{{ asset_url('dist/chat-client.js') }}` +- Als het manifest onverhoopt ontbreekt (bijv. in een edge-case), valt `asset_url` automatisch terug naar het on-gehashte pad onder `/static/` zodat de pagina niet breekt. + +## Nginx (DEV/TEST) + +In `nginx/nginx.conf` is toegevoegd: + +``` +location ^~ /static/dist/ { + alias /etc/nginx/static/dist/; + add_header Cache-Control "public, max-age=31536000, immutable" always; +} +``` + +- Hiermee krijgen gehashte assets lange caching. Omdat de URL wijzigt bij iedere inhoudswijziging, halen browsers/CDN de nieuwe versie zonder problemen op. +- HTML wordt niet agressief gecachet door Nginx; wil je volledig no-cache afdwingen voor HTML, dan kun je dat ook applicatie‑zijde doen (Flask) of via extra Nginx rules voor specifieke HTML routes. + +## Ingress (STAGING/PROD) en Bunny.net + +- Ingress: Zet (indien nog niet aanwezig) voor het pad dat `/static/dist/` serveert een configuration snippet met dezelfde header: + - `add_header Cache-Control "public, max-age=31536000, immutable" always;` +- HTML/endpoints laat je kort/no-cache of standaard; belangrijk is dat templates met nieuwe gehashte URLs snel door de clients worden opgepakt. +- Bunny.net (pull zone): CDN cachet op basis van volledige URL. Door de content-hash in de bestandsnaam zijn purges normaliter niet nodig. + +## Troubleshooting / Tips + +- Controleer na build of `manifest.json` bestaat: + - `ls nginx/static/dist/manifest.json` +- Inspecteer headers in DEV: + - `curl -I http:///static/dist/chat-client..js` + - Moet `Cache-Control: public, max-age=31536000, immutable` tonen. +- Als een client toch oude CSS/JS toont: + - Check of de geleverde HTML naar de nieuwste gehashte bestandsnaam verwijst (View Source / Network tab). + - Zorg dat de HTML niet op CDN langdurig wordt gecachet. + +## Samenvatting workflow per omgeving + +- DEV/TEST (Nginx): + 1) `docker/rebuild_chat_client.sh` draaien (of handmatig `npm run build` in `nginx/`). + 2) Nginx container wordt vernieuwd met nieuwe `static/dist` inclusief manifest en gehashte bundles. + +- STAGING/PROD (Ingress + Bunny): + 1) Zelfde build, gehashte bundles en manifest worden gedeployed via container image. + 2) Ingress dient `/static/dist/` met lange cache headers. + 3) Bunny.net cachet de nieuwe URLs automatisch; purgen niet nodig. + +Met deze setup combineren we maximale performance (lange caching) met directe updates (nieuwe URL bij wijziging). diff --git a/documentation/migratie_startup_en_builds.md b/documentation/migratie_startup_en_builds.md new file mode 100644 index 0000000..84fb11b --- /dev/null +++ b/documentation/migratie_startup_en_builds.md @@ -0,0 +1,208 @@ +# Migratieplan: Standaardisatie van Startup Processen en Docker Builds + +Dit document beschrijft de migratie-afspraken rond het opstarten van onze applicaties, inclusief het gebruik van een generiek startscript, het inzetten van `tini` als entrypoint, en de overstap naar een **shared base build** structuur. + +Doel: **standaardisatie**, **betere betrouwbaarheid in Kubernetes**, en **snellere builds**. + +--- + +## 1. Generiek startscript (`scripts/start.sh`) + +### Doel +- EΓ©n startscript voor **alle rollen**: web, worker, beat. +- Gedrag wordt bepaald via **environment variables** (`ROLE=web|worker|beat`). +- Geen β€œmagische” verschillen meer tussen Podman en Kubernetes. + +### Voorbeeld +```bash +#!/usr/bin/env bash +set -euo pipefail + +ROLE="${ROLE:-web}" + +case "$ROLE" in + web) + exec gunicorn -w "${WORKERS:-1}" -k "${WORKER_CLASS:-gevent}" -b "0.0.0.0:${PORT:-8080}" --worker-connections "${WORKER_CONN:-100}" --access-logfile - --error-logfile - --log-level "${LOGLEVEL:-info}" scripts.run_eveai_app:app + ;; + + worker) + exec celery -A scripts.run_eveai_workers worker --loglevel="${CELERY_LOGLEVEL:-INFO}" --concurrency="${CELERY_CONCURRENCY:-2}" --max-tasks-per-child="${CELERY_MAX_TASKS_PER_CHILD:-1000}" --prefetch-multiplier="${CELERY_PREFETCH:-1}" -O fair + ;; + + beat) + exec celery -A scripts.run_eveai_workers beat --loglevel="${CELERY_LOGLEVEL:-INFO}" + ;; + + *) + echo "Unknown ROLE=$ROLE" >&2 + exit 1 + ;; +esac +``` + +### Belangrijk +- **Geen init/migraties** meer in het startscript (die draaien we via Jobs/CronJobs). +- **Geen `cd`, `chown`, of PYTHONPATH-hacks** in startscript β†’ alles naar Dockerfile. +- **Altijd `exec`** gebruiken zodat processen signalen correct ontvangen. + +--- + +## 2. Gebruik van `tini` als ENTRYPOINT + +### Waarom? +- In containers draait het eerste proces als **PID 1**. +- Zonder init-proces worden signalen niet correct doorgegeven en ontstaan **zombieprocessen**. +- `tini` is een lichtgewicht init die dit oplost. + +### Implementatie +```dockerfile +RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["/usr/bin/tini","-g","--"] +CMD ["bash","-lc","scripts/start.sh"] +``` + +- `-g` = stuur signalen naar de hele process group (belangrijk voor Gunicorn en Celery). +- Hiermee is een apart `entrypoint.sh` niet meer nodig. + +--- + +## 3. Verplaatsen van logica naar Dockerfile + +### Wat naar de Dockerfile moet +- `WORKDIR /app` +- `ENV PYTHONPATH=/app:/app/patched_packages:$PYTHONPATH` +- `ENV FLASK_APP=/app/scripts/run_eveai_app.py` (alleen nodig voor CLI, niet voor gunicorn) +- User-aanmaak en permissies: + ```dockerfile + ARG UID=10001 + ARG GID=10001 + RUN groupadd -g ${GID} appuser && useradd -u ${UID} -g ${GID} -M -d /nonexistent -s /usr/sbin/nologin appuser + RUN chown -R appuser:appuser /app + USER appuser + ``` + +### Wat niet meer in startscript hoort +- `cd /app` +- `export PYTHONPATH=...` +- `chown -R ... /logs` +- DB-migraties of cache-invalidatie β†’ deze verhuizen naar **Kubernetes Jobs/CronJobs**. + +--- + +## 4. Kubernetes Jobs & CronJobs + +### Use cases +- **Jobs** β†’ eenmalige taken zoals DB-migraties of cache-invalidatie. +- **CronJobs** β†’ geplande taken (bv. nachtelijke opschoningen). + +### Waarom? +- Startup scripts blijven lean. +- Geen race conditions bij meerdere replicas. +- Flexibel los van deploys uit te voeren. + +--- + +## 5. Docker builds: naar een shared base (Optie B) + +### Nieuwe structuur +``` +repo/ +β”œβ”€ Dockerfile.base +β”œβ”€ requirements.txt +β”œβ”€ common/ +β”œβ”€ config/ +β”œβ”€ scripts/ +β”œβ”€ patched_packages/ +β”œβ”€ docker/ +β”‚ β”œβ”€ eveai_app/Dockerfile +β”‚ β”œβ”€ eveai_api/Dockerfile +β”‚ └─ eveai_workers/Dockerfile +└─ compose_dev.yaml +``` + +### Dockerfile.base +```dockerfile +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 + +RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* + +ARG UID=10001 +ARG GID=10001 +RUN groupadd -g ${GID} appuser && useradd -u ${UID} -g ${GID} -M -d /nonexistent -s /usr/sbin/nologin appuser + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY common /app/common +COPY config /app/config +COPY scripts /app/scripts +COPY patched_packages /app/patched_packages + +RUN chown -R appuser:appuser /app && chmod +x /app/scripts/start.sh + +ENV PYTHONPATH=/app:/app/patched_packages:${PYTHONPATH} ROLE=web PORT=8080 + +USER appuser +EXPOSE 8080 + +ENTRYPOINT ["/usr/bin/tini","-g","--"] +CMD ["bash","-lc","scripts/start.sh"] +``` + +### Service Dockerfiles + +#### docker/eveai_app/Dockerfile +```dockerfile +FROM yourorg/eveai-base:py312-v1 +WORKDIR /app +COPY eveai_app /app/eveai_app +COPY migrations /app/migrations +COPY content /app/content +ENV ROLE=web PORT=8080 +``` + +#### docker/eveai_api/Dockerfile +```dockerfile +FROM yourorg/eveai-base:py312-v1 +WORKDIR /app +COPY eveai_api /app/eveai_api +ENV ROLE=web PORT=8080 +``` + +#### docker/eveai_workers/Dockerfile +```dockerfile +FROM yourorg/eveai-base:py312-v1 +WORKDIR /app +COPY eveai_workers /app/eveai_workers +ENV ROLE=worker +``` + +--- + +## 6. Workflow afspraken + +1. **Startscript** is generiek β†’ gedrag via env (`ROLE=...`). +2. **Geen init-taken** meer in startscript β†’ Jobs/CronJobs in k8s. +3. **`tini` als ENTRYPOINT** β†’ correcte signalen en zombie-cleanup. +4. **Dockerfile** regelt PYTHONPATH, FLASK_APP, permissies en user. +5. **Base image** bevat Python, deps en common code. +6. **Service-images** kopiΓ«ren enkel hun eigen directories. +7. **Build flow**: + - Base builden/pushen bij gewijzigde deps/common. + - Services snel erbovenop rebuilden bij code-wijzigingen. + +--- + +## 7. Samenvatting + +Met deze migratie krijgen we: +- Eenvoudiger startscript (alleen proceskeuze). +- Betere betrouwbaarheid bij shutdowns (via tini). +- Strikte scheiding tussen **lifecycle taken** en **runtime**. +- Snellere builds via shared base image. +- Uniforme aanpak in zowel Podman/Compose als Kubernetes. + diff --git a/documentation/mobile-ux.md b/documentation/mobile-ux.md new file mode 100644 index 0000000..21b02c4 --- /dev/null +++ b/documentation/mobile-ux.md @@ -0,0 +1,108 @@ +# Mobile UX Improvements for EveAI Chat Client + +This document explains the changes we made to improve the mobile user experience and how to verify or tweak them. + +## Summary of Issues Addressed +- Unwanted white margins and page scroll on mobile due to default body margin and mixed vh/dvh usage. +- Layout jitter and incorrect heights when the browser address bar or the on-screen keyboard appears. +- Chat input potentially being obscured by the keyboard. +- Differences between DevTools emulation and real devices. + +## Key Changes + +1. CSS base reset and layout fixes +- Remove default white margins and prevent double scroll. +- Ensure outer containers don’t create conflicting scrollbars. +- Respect device safe-area insets (bottom notch, etc.). + +Files: +- eveai_chat_client/templates/base.html + - Added a CSS custom property fallback: `--vvh: 1vh;` on `:root`. +- eveai_chat_client/static/assets/css/chat.css + - `.app-container` now uses `height: calc(var(--vvh, 1vh) * 100);`. + - Added base reset: `body { margin: 0; overflow: hidden; }` and global `box-sizing: border-box`. + - Mobile `.content-area` no longer uses `calc(100vh - header)`; we rely on flex with `min-height: 0`. +- eveai_chat_client/static/assets/css/chat-components.css + - Added `padding-bottom: env(safe-area-inset-bottom)` for the chat input area on mobile. + - When keyboard is open (detected via JS), we slightly increase bottom padding. + +2. VisualViewport utility and debug overlay +- We introduced a small utility that: + - Sets a CSS variable `--vvh` equal to 1% of the current visual viewport height. + - Toggles a `keyboard-open` class on `` when the keyboard is likely visible. + - Optionally shows a debug overlay with real-time viewport metrics. + +Files: +- frontend_src/js/utils/viewport.js (new) + - Exports `initViewportFix({ enableDebug })`. + - Updates `--vvh` on resize/scroll/orientation changes. + - Optional debug overlay via `enableDebug`. +- frontend_src/js/chat-client.js + - Imports and initializes the util on DOMContentLoaded. + - Enable overlay via query parameter: `?debug=viewport`. + +## How It Works +- CSS reads `--vvh` to size the vertical layout. This follows the visual viewport, so when the URL bar or keyboard affects the viewport, the chat layout adapts without extra page scroll. +- The `keyboard-open` body class allows us to slightly increase bottom padding for the input to avoid edge cases where the input is near the keyboard. +- On mobile, `.app-container` spans the full visual viewport height, and `.content-area` flexes to take remaining space under the mobile header without relying on height calculations. + +## Testing Checklist +1. Open the chat on real devices (iOS Safari, Android Chrome). +2. Verify there are no white margins around the app; the overall page should not scroll. +3. Focus the message input: + - Keyboard appears; the message list should stay usable and the input should remain visible. + - The page should not jump in height unexpectedly. +4. Rotate device (portrait/landscape) and ensure the layout adapts. +5. Try with/without the browser UI (scroll a bit to collapse the address bar on Android Chrome). +6. Optional: enable overlay with `?debug=viewport` and verify reported values look sane. + +## Tuning and Fallbacks +- If you need to support extremely old iOS versions that lack VisualViewport, we still fall back to `1vh` for `--vvh`. You can increase robustness by setting `--vvh` via a simple `window.innerHeight` measurement on load. +- Keyboard detection is heuristic-based (drop > 120px). Adjust the threshold in `viewport.js` if needed for specific device groups. +- Avoid adding fixed `height: 100vh` or `calc(100vh - X)` to new containers; prefer flex layouts with `min-height: 0` for scrollable children. + +## Known Limitations +- On some embedded browsers (in-app webviews), viewport behavior can deviate; test with your target apps. +- The safe-area env variables require modern browsers; most iOS/Android are fine, but always test. + +## Enabling/Disabling Debug Overlay +- Append `?debug=viewport` to the URL to enable. +- The overlay displays: `innerHeight`, `clientHeight`, `visualViewport.height`, `visualViewport.pageTop`, header height, current `--vvh`, and keyboard state. + +## Next Steps (Optional Enhancements) +- PWA manifest with `display: standalone` to reduce address bar effects. +- Add smooth scroll-into-view on input focus for edge cases on particular webviews. +- Add E2E tests using BrowserStack for a small device matrix. + +## Focus Zoom Prevention (2025-09-24) +Some mobile browsers (notably iOS Safari) auto-zoom when focusing inputs with small font sizes. To prevent this: +- We enforce font-size: 16px for inputs, selects, textareas, and buttons on mobile in chat-components.css. +- We stabilize text scaling via `-webkit-text-size-adjust: 100%`. +- We extended the meta viewport with `viewport-fit=cover`. +- The debug overlay now also shows `visualViewport.scale` to confirm no zoom is happening (expect 1.00 during focus). + +Files: +- eveai_chat_client/static/assets/css/chat-components.css (mobile form control font-size) +- eveai_chat_client/static/assets/css/chat.css (text-size-adjust) +- eveai_chat_client/templates/base.html (viewport meta) +- frontend_src/js/utils/viewport.js (overlay shows vv.scale) + +Validation checklist: +- Focus the chat input and language select on iOS Safari and Android Chrome; the UI should not scale up. +- Overlay shows `vv.scale: 1.00` with `?debug=viewport`. + +## Mobile Header Overflow Fix (2025-09-24) +We observed that the mobile header could push the layout wider than the viewport on small devices, causing horizontal page scroll. We addressed this by: +- Allowing the header row to wrap: `.mobile-header { flex-wrap: wrap; }`. +- Ensuring children can shrink within the container: `min-width: 0` on `.mobile-logo` and `.mobile-language-selector`. +- Constraining controls: `:deep(.language-select)` uses `max-width: 100%` and on very small screens `max-width: 60vw`. +- Preventing accidental bleed: `.mobile-header { max-width: 100%; overflow: hidden; }`. +- Added `html { overflow-x: hidden; }` as a light global failsafe. + +Files: +- eveai_chat_client/static/assets/vue-components/MobileHeader.vue (scoped styles updated) +- eveai_chat_client/static/assets/css/chat.css (added html { overflow-x: hidden; }) + +Validation checklist: +- On 320px width devices, header may wrap to 2 lines without horizontal scroll. +- Long tenant names and multiple languages do not cause page-wide overflow; content remains within viewport width. diff --git a/eveai_app/templates/head.html b/eveai_app/templates/head.html index 54c35cf..4e4fda6 100644 --- a/eveai_app/templates/head.html +++ b/eveai_app/templates/head.html @@ -13,7 +13,7 @@ - + diff --git a/eveai_app/templates/scripts.html b/eveai_app/templates/scripts.html index c94753d..1397bd8 100644 --- a/eveai_app/templates/scripts.html +++ b/eveai_app/templates/scripts.html @@ -1,5 +1,5 @@ {#dist/main.js contains all used javascript libraries#} - + {% include 'eveai_json_editor.html' %} {% include 'eveai_ordered_list_editor.html' %} diff --git a/eveai_chat_client/static/assets/css/chat-components.css b/eveai_chat_client/static/assets/css/chat-components.css index 22d0d0f..9d8f408 100644 --- a/eveai_chat_client/static/assets/css/chat-components.css +++ b/eveai_chat_client/static/assets/css/chat-components.css @@ -79,6 +79,16 @@ .chat-input-area { max-width: 100%; /* Op mobiel volledige breedte gebruiken */ + /* Respect safe-area at bottom */ + padding-bottom: env(safe-area-inset-bottom, 0); + } + + /* Prevent iOS focus zoom by ensuring form controls are >= 16px */ + input, textarea, select, button, + .chat-input input, .chat-input textarea, .chat-input select, .chat-input button, + .mobile-language-selector :is(select, input, button) { + font-size: 16px; + line-height: 1.4; } .chat-input { @@ -97,6 +107,11 @@ } } +/* When keyboard is open, allow a bit more space below */ +body.keyboard-open .chat-input-area { + padding-bottom: calc(env(safe-area-inset-bottom, 0) + 8px); +} + /* Extra small screens */ @media (max-width: 480px) { .chat-app-container { @@ -251,15 +266,6 @@ margin: 20px 0; } -.form-message .message-content { - max-width: 90%; - background: white; - border: 1px solid #e9ecef; - border-radius: 12px; - padding: 20px; - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - /* System messages */ .system-message { text-align: center; @@ -437,24 +443,6 @@ opacity: 0.8; } -/* Mobile responsiveness */ -@media (max-width: 768px) { - .message { - padding: 0 15px; - } - - /* moved to ChatMessage.vue scoped styles */ - - .message.user .message-content { - margin-left: 40px; - } - - .message.ai .message-content, - .message.bot .message-content { - margin-right: 40px; - } -} - @media (max-width: 480px) { .message { padding: 0 10px; diff --git a/eveai_chat_client/static/assets/css/chat-input.css b/eveai_chat_client/static/assets/css/chat-input.css index 2854f7e..39d8461 100644 --- a/eveai_chat_client/static/assets/css/chat-input.css +++ b/eveai_chat_client/static/assets/css/chat-input.css @@ -63,13 +63,6 @@ margin-top: 10px; } -/* Dynamic form container transitions */ -.dynamic-form-container { - transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; - transform: translateY(0); - opacity: 1; -} - /* Chat input transitions */ .chat-input { transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; diff --git a/eveai_chat_client/static/assets/css/chat-message.css b/eveai_chat_client/static/assets/css/chat-message.css index a84e026..34a4cff 100644 --- a/eveai_chat_client/static/assets/css/chat-message.css +++ b/eveai_chat_client/static/assets/css/chat-message.css @@ -146,11 +146,6 @@ font-size: 14px; } -/* Ensure forms in messages use full available width */ -.message .dynamic-form-container { - width: 100%; - max-width: none; -} .message .dynamic-form { width: 100%; diff --git a/eveai_chat_client/static/assets/css/chat.css b/eveai_chat_client/static/assets/css/chat.css index ce1cfe5..11daad1 100644 --- a/eveai_chat_client/static/assets/css/chat.css +++ b/eveai_chat_client/static/assets/css/chat.css @@ -14,8 +14,9 @@ /* App container layout */ .app-container { display: flex; - height: 100vh; /* fallback */ - height: 100dvh; /* prefer dynamic viewport unit */ + /* Use visual viewport variable when available */ + min-height: 0; + height: calc(var(--vvh, 1vh) * 100); width: 100%; } @@ -85,8 +86,7 @@ display: flex; flex-direction: column; min-height: 0; - height: 100vh; /* fallback for desktop */ - height: 100dvh; /* prefer dynamic viewport on desktop */ + height: auto; /* prefer dynamic viewport on desktop */ } .chat-container { @@ -97,16 +97,19 @@ } html, body { - height: 100%; + height: calc(var(--vvh, 1vh) * 100); min-height: 0; } +/* Base reset & overflow control */ +* { box-sizing: border-box; } +html { overflow-x: hidden; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } body { + margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: var(--text-color); background-color: var(--background-color); - height: 100vh; - overflow: hidden; + overflow: hidden; /* prevent double scroll; inner containers handle scrolling */ } .container { @@ -281,14 +284,6 @@ body { } :root { --mobile-header-height: 60px; } /* default/minimum */ - - /* Content area takes remaining space */ - .content-area { - flex: 1; - height: calc(100vh - var(--mobile-header-height)); /* fallback */ - height: calc(100dvh - var(--mobile-header-height)); /* prefer dynamic viewport */ - min-height: 0; - } } /* Responsieve design regels worden nu gedefinieerd in chat-components.css */ diff --git a/eveai_chat_client/static/assets/css/form.css b/eveai_chat_client/static/assets/css/form.css index 69c3569..dc39279 100644 --- a/eveai_chat_client/static/assets/css/form.css +++ b/eveai_chat_client/static/assets/css/form.css @@ -1,9 +1,5 @@ /* Dynamisch formulier stijlen */ -.dynamic-form { - padding: 15px; -} - .form-header { display: flex; align-items: center; diff --git a/eveai_chat_client/static/assets/vue-components/ChatInput.vue b/eveai_chat_client/static/assets/vue-components/ChatInput.vue index cc55a8a..9f1f210 100644 --- a/eveai_chat_client/static/assets/vue-components/ChatInput.vue +++ b/eveai_chat_client/static/assets/vue-components/ChatInput.vue @@ -434,13 +434,13 @@ export default { .chat-input-container { width: 100%; max-width: 1000px; - padding: 20px; + padding: 10px; box-sizing: border-box; background-color: var(--active-background-color); color: var(--human-message-text-color); border-top: 1px solid #e0e0e0; font-family: Arial, sans-serif; - font-size: 14px; + font-size: 16px; transition: opacity 0.2s ease-in-out; margin-left: auto; margin-right: auto; @@ -470,7 +470,7 @@ export default { outline: none; transition: border-color 0.2s; font-family: Arial, sans-serif; - font-size: 14px; + font-size: 16px; /* Transparante achtergrond in plaats van wit */ background-color: var(--human-message-background); color: var(--human-message-text-color); @@ -534,14 +534,14 @@ export default { /* Active AI Message Area - positioned at top of ChatInput */ .active-ai-message-area { - margin-bottom: 15px; - padding: 12px; + margin-bottom: 10px; background-color: var(--ai-message-background); color: var(--ai-message-text-color); border-radius: 8px; font-family: Arial, sans-serif; - font-size: 14px; + font-size: 16px; box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); + padding: 5px; } /* Ensure the active AI message integrates well with ChatInput styling */ diff --git a/eveai_chat_client/static/assets/vue-components/ChatMessage.vue b/eveai_chat_client/static/assets/vue-components/ChatMessage.vue index b206737..eba33d9 100644 --- a/eveai_chat_client/static/assets/vue-components/ChatMessage.vue +++ b/eveai_chat_client/static/assets/vue-components/ChatMessage.vue @@ -432,7 +432,7 @@ export default { .message-content { font-family: Arial, sans-serif; font-size: 12px; - max-width: 90%; + max-width: 100%; padding: 8px; border-radius: 10px; word-wrap: break-word; @@ -663,8 +663,7 @@ export default { .message.bot .message-content { background: var(--ai-message-background); color: var(--ai-message-text-color); - border-bottom-left-radius: 4px; - margin-right: 60px; + margin-right: 20px; } /* Hover effects for message bubbles */ @@ -692,27 +691,18 @@ export default { } .message.ai .message-content, .message.bot .message-content { - margin-right: 40px; + margin-right: 5px; } /* Mobile: place logo inside bubble and prevent overlap with text */ .message.ai .ai-message-logo { top: -12px; - left: 8px; width: 24px; height: 24px; } .message.ai .message-content { /* Reserve space for the in-bubble logo */ padding-top: 1px; /* 24px logo + margins */ - padding-left: 1px; - } -} - -@media (max-width: 480px) { - .message-content { - max-width: 90%; - margin-left: 20px !important; - margin-right: 20px !important; + padding-left: 10px; } } diff --git a/eveai_chat_client/static/assets/vue-components/DynamicForm.vue b/eveai_chat_client/static/assets/vue-components/DynamicForm.vue index 4b03516..6a49568 100644 --- a/eveai_chat_client/static/assets/vue-components/DynamicForm.vue +++ b/eveai_chat_client/static/assets/vue-components/DynamicForm.vue @@ -538,14 +538,20 @@ export default {