diff --git a/.env.example b/.env.example index c661be4..37a4b02 100644 --- a/.env.example +++ b/.env.example @@ -47,6 +47,16 @@ LOCALSTACK_AWS_SECRET_ACCESS_KEY=test LOCALSTACK_AWS_SESSION_TOKEN= +# ----------------------------------------------------------------------------- +# Frontend SPA statico locale (S3 LocalStack + CloudFront locale) +# ----------------------------------------------------------------------------- +# Queste variabili riguardano SOLO il serving della SPA Angular. +# Non usarle per i documenti e non sostituirle con bucket S3 reali. +# I documenti restano governati da POC_DOCUMENT_DISK/AWS_REAL_* più sotto. +FRONTEND_STATIC_BUCKET=poc-frontend-static-local +FRONTEND_CLOUDFRONT_LOCAL_URL=https://localhost:8443 + + # ----------------------------------------------------------------------------- # Porte pubblicate sull'host (lato sinistro = porta sulla tua macchina) # ----------------------------------------------------------------------------- @@ -194,4 +204,4 @@ TEXTRACT_MAX_BYTES=0 BEDROCK_ENABLED=true BEDROCK_REGION= BEDROCK_ENDPOINT= -BEDROCK_MODEL_ID=amazon.nova-lite-v1:0 \ No newline at end of file +BEDROCK_MODEL_ID=amazon.nova-lite-v1:0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d90f418..7b5ec63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ on: branches: - develop - main - - migration/new_techs + - migrate/react_angular tags: - "v*" pull_request: @@ -131,6 +131,9 @@ jobs: - name: Check generated client is committed run: git diff --exit-code -- apps/frontend/src/api/generated + - name: Lint SPA + run: docker compose --profile tools run --rm node npm run frontend:lint + - name: Typecheck SPA run: docker compose --profile tools run --rm node npm run frontend:typecheck @@ -190,7 +193,9 @@ jobs: # L'archivio viene caricato in cache dal post-step di actions/cache - name: Save image archive for cache if: steps.image-cache.outputs.cache-hit != 'true' - run: docker save $(cat /tmp/ci-images.txt) | zstd -T0 -o /tmp/ci-images.tar.zst + run: | + mapfile -t images < /tmp/ci-images.txt + docker save "${images[@]}" | zstd -T0 -o /tmp/ci-images.tar.zst # --- Check statici senza build (fail fast) --- @@ -251,6 +256,15 @@ jobs: - name: Apply LocalStack infrastructure run: docker compose --profile tools run --rm terraform init && docker compose --profile tools run --rm terraform apply -auto-approve + # Il percorso di serving default e' Traefik -> frontend-cloudfront -> S3 + # LocalStack: lo smoke e gli audit a11y devono colpire la SPA realmente + # caricata nel bucket, non un bucket vuoto. Si riusa la stessa logica di + # make setup (build + sync con cache-control differenziato). + - name: Build and deploy Angular SPA to LocalStack S3 + run: | + make frontend-build + make frontend-s3-local-upload + - name: Run migrations run: docker compose --profile release run --rm migrate @@ -295,6 +309,11 @@ jobs: echo "$spa_headers" | grep -i '^content-security-policy:' | grep -F "frame-ancestors 'none'" echo "$spa_headers" | grep -i '^x-content-type-options: nosniff' echo "$spa_headers" | grep -i '^referrer-policy: strict-origin-when-cross-origin' + + # La SPA Angular deve essere servita da S3 LocalStack via frontend-cloudfront, + # sia sulla root sia su un deep link (fallback SPA verso index.html) + curl --silent --fail --insecure https://localhost:8443/ | grep -q ' + Laravel API + Angular SPA + PostgreSQL + LocalStack + Bedrock + OpenTelemetry +

+ +

+ CI (main) +

+ Proof of Concept per workflow HR e documentali assistiti da AI, con generazione di comunicazioni, pipeline asincrona di elaborazione PDF, integrazione AWS-like locale e osservabilità end-to-end. - - - - - - - -
- Application

- Laravel -
- PHP -
- React -
- TypeScript -
- Data & Storage

- PostgreSQL -
- Redis -
- S3-compatible storage -
- Workflow & AI

- LocalStack -
- Step Functions -
- SQS -
- Bedrock -
- Observability

- OpenTelemetry -
- Prometheus -
- Grafana -
- Loki and Tempo -
- - - - - -
- CI / Quality Gate

- Pest - Vitest - Trivy - Accessibility - Terraform

- CI (main) -
## Contesto @@ -71,12 +28,13 @@ Il valore principale del progetto è architetturale e dimostrativo: ogni fase de La PoC dimostra un modello applicativo composto da più livelli cooperanti: -* una **SPA React/Vite/TypeScript** per l’interazione operatore; +* una **SPA Angular/TypeScript** per l’interazione operatore; * un backend **Laravel/PHP** per API, validazione, autorizzazione, orchestrazione applicativa e persistenza; * **PostgreSQL** per dati strutturati, stati applicativi, audit e risultati di elaborazione; * **Redis** per cache, sessioni e rate limiting; * storage documentale **S3-compatible** per PDF originali e sotto-documenti generati; * **LocalStack** per emulare localmente servizi AWS come SQS, Step Functions, SSM, Secrets Manager e S3; +* **CloudFront locale** davanti al bucket S3 LocalStack per il serving della SPA Angular; * integrazione AI tramite astrazione verso **Bedrock** e integrazione OCR tramite **Textract** (attivabile, disabilitata di default); * stack di osservabilità con **OpenTelemetry, Prometheus, Grafana, Tempo, Loki, Alloy e Alertmanager**; * CI con test backend/frontend, scansione immagini, validazione infrastrutturale e audit accessibilità. @@ -87,7 +45,7 @@ La separazione tra richiesta HTTP e workflow asincrono è uno dei punti centrali L’architettura locale è organizzata intorno a un entrypoint edge, un layer applicativo, servizi dati, workflow asincroni e osservabilità. -Traefik gestisce l’ingresso verso i servizi esposti e instrada il traffico verso Nginx. Nginx serve la SPA frontend e inoltra le richieste API al runtime Laravel/PHP-FPM. Laravel gestisce le API, valida le richieste, applica le regole applicative, registra eventi di audit e avvia i workflow documentali. PostgreSQL conserva lo stato persistente, Redis supporta componenti runtime a bassa latenza, mentre LocalStack fornisce servizi AWS-like in ambiente locale. +Traefik gestisce l’ingresso verso i servizi esposti e instrada il traffico applicativo verso il CloudFront locale. Il CloudFront locale serve la SPA Angular dagli oggetti caricati nel bucket S3 LocalStack e inoltra `/api/`, `/health` e `/ready` a Nginx/Laravel. Nginx resta il proxy applicativo verso PHP-FPM e il percorso interno di compatibilità. PostgreSQL conserva lo stato persistente, Redis supporta componenti runtime a bassa latenza, mentre LocalStack fornisce servizi AWS-like in ambiente locale. I worker Laravel consumano task asincroni da SQS e comunicano con Step Functions tramite callback task token. Questo permette di rappresentare una pipeline documentale composta da stati espliciti, retry, gestione errori, idempotenza e aggiornamento progressivo dello stato. @@ -152,6 +110,18 @@ make test make logs ``` +### Serving statico LocalStack + +```bash +make frontend-s3-local-deploy +make frontend-cloudfront-local-url +make frontend-serving-local-test +``` + +Il flusso builda Angular, provisiona il bucket S3 LocalStack via Terraform, carica `apps/frontend/dist` con cache-control differenziato (`index.html` no-cache, bundle hashati immutable) e verifica il serving attraverso il CloudFront locale su `https://localhost:8443`. La simulazione valida il pattern build → bucket → distribuzione CDN-like locale, ma non sostituisce una validazione CloudFront reale con TLS/OAC/edge propagation/invalidation AWS. + +Il bucket `FRONTEND_STATIC_BUCKET` è dedicato solo alla SPA. I documenti continuano a usare `POC_DOCUMENT_DISK=s3` per S3 LocalStack o `POC_DOCUMENT_DISK=real_s3` con `AWS_REAL_*` per S3/Textract reali. + ### Accesso ai servizi | Servizio | URL | diff --git a/app/Console/Commands/ResetPocData.php b/app/Console/Commands/ResetPocData.php index 56ee185..428da3f 100644 --- a/app/Console/Commands/ResetPocData.php +++ b/app/Console/Commands/ResetPocData.php @@ -96,6 +96,11 @@ private function resetStorage(): void { $documentDisk = config('filesystems.default', 'local'); + // I documenti sono salvati in "originals/" e "sub/" relativi al root del + // disk (vedi DocumentProcessingService); "documents/" resta per pulire + // eventuali dati pregressi del vecchio layout. + Storage::disk($documentDisk)->deleteDirectory('originals'); + Storage::disk($documentDisk)->deleteDirectory('sub'); Storage::disk($documentDisk)->deleteDirectory('documents'); Storage::disk($documentDisk)->deleteDirectory('livewire-tmp'); diff --git a/app/Copilot/Documents/Enums/ReviewStatus.php b/app/Copilot/Documents/Enums/ReviewStatus.php index 99ef9ca..f86a704 100644 --- a/app/Copilot/Documents/Enums/ReviewStatus.php +++ b/app/Copilot/Documents/Enums/ReviewStatus.php @@ -23,9 +23,9 @@ public function color(): string { return match ($this) { self::NeedsReview => 'warning', - self::AutoValidated => 'success', + self::AutoValidated => 'info', self::Quarantined => 'danger', - self::ManuallyValidated => 'info', + self::ManuallyValidated => 'success', }; } } diff --git a/app/Copilot/Documents/Services/DocumentProcessingService.php b/app/Copilot/Documents/Services/DocumentProcessingService.php index 6e26c87..aad3572 100644 --- a/app/Copilot/Documents/Services/DocumentProcessingService.php +++ b/app/Copilot/Documents/Services/DocumentProcessingService.php @@ -34,7 +34,7 @@ public function __construct( */ public function storeUpload(UploadedFile $file, PocUser $actor): OriginalDocument { - $path = $file->store('documents/originals', $this->documentDisk()); + $path = $file->store('originals', $this->documentDisk()); if (! is_string($path) || $path === '') { throw new \RuntimeException('Impossibile salvare il documento nello storage configurato.'); @@ -535,7 +535,7 @@ private function extractPages(string $absoluteSource, int $originalId, string $e } $slug = preg_replace('/[^a-z0-9_]/i', '_', $employeeName) ?: 'documento'; - $relativePath = "documents/sub/{$originalId}_{$slug}_{$startPage}-{$endPage}_".Str::uuid().'.pdf'; + $relativePath = "sub/{$originalId}_{$slug}_{$startPage}-{$endPage}_".Str::uuid().'.pdf'; $pdf->Output($absoluteDest, 'F'); diff --git a/app/Copilot/Support/PocStateService.php b/app/Copilot/Support/PocStateService.php index 0d94c11..2054bf7 100644 --- a/app/Copilot/Support/PocStateService.php +++ b/app/Copilot/Support/PocStateService.php @@ -101,8 +101,6 @@ public function document(SubDocument $subDocument): array $pages = max(1, ((int) $subDocument->end_page - (int) $subDocument->start_page) + 1); $previewLines = [ 'Split iniziale: pagine '.$subDocument->start_page.'-'.$subDocument->end_page.'.', - 'File originale: '.($original?->original_filename ?: 'Non disponibile').'.', - 'Campi rilevati dal servizio AI configurato.', ]; if ($subDocument->error_message) { @@ -128,7 +126,10 @@ public function document(SubDocument $subDocument): array 'reviewStatus' => $subDocument->review_status->value, 'reviewStatusLabel' => $subDocument->review_status->label(), 'error' => $subDocument->error_message, - 'previewUrl' => route('api.v1.documents.preview', ['subDocument' => $subDocument->id]), + // URL relativo: vedi nota in DocumentController::store. Un URL assoluto + // sarebbe generato con schema "http://" dietro Traefik e bloccato dal + // browser (mixed-content sull'iframe e sul fetch dell'anteprima). + 'previewUrl' => route('api.v1.documents.preview', ['subDocument' => $subDocument->id], false), 'previewLines' => $previewLines, ]; } diff --git a/app/Copilot/Workflow/Services/DocumentWorkflowService.php b/app/Copilot/Workflow/Services/DocumentWorkflowService.php index 66f5849..9b70201 100644 --- a/app/Copilot/Workflow/Services/DocumentWorkflowService.php +++ b/app/Copilot/Workflow/Services/DocumentWorkflowService.php @@ -178,10 +178,10 @@ private function documentKey(OriginalDocument $document): string $disk = (string) config('poc.documents.storage_disk', config('filesystems.default')); $root = trim((string) config("filesystems.disks.{$disk}.root", ''), '/'); - // file_path is always disk-relative (no root prefix), while Textract needs - // the raw S3 key, which includes the disk root. Always prepend it; the - // previous str_starts_with shortcut was fooled when the upload folder - // happened to start with the root (e.g. root "documents" + "documents/originals"). + // file_path is always disk-relative (no root prefix, e.g. "originals/..", + // "sub/.."), while Textract needs the raw S3 key, which includes the disk + // root. Always prepend it: root "documents" + "originals/x" -> the actual + // key "documents/originals/x". return $root === '' ? $document->file_path : $root.'/'.$document->file_path; } diff --git a/app/Http/Controllers/Api/V1/Copilot/DocumentController.php b/app/Http/Controllers/Api/V1/Copilot/DocumentController.php index 9ac65f8..04811df 100644 --- a/app/Http/Controllers/Api/V1/Copilot/DocumentController.php +++ b/app/Http/Controllers/Api/V1/Copilot/DocumentController.php @@ -43,7 +43,10 @@ public function store(UploadDocumentRequest $request, DocumentProcessingService return response()->json([ 'message' => 'Documento caricato. Workflow documentale avviato.', - 'streamUrl' => route('api.v1.documents.stream', $original), + // URL relativo: la SPA e' servita in HTTPS dietro Traefik, che termina il + // TLS e inoltra in HTTP. Un URL assoluto verrebbe generato con schema + // "http://" e bloccato dal browser come mixed-content / CSP connect-src. + 'streamUrl' => route('api.v1.documents.stream', $original, false), ], 202); } @@ -67,9 +70,20 @@ public function stream(Request $request, OriginalDocument $originalDocument, Poc flush(); }; + // Commento SSE: ignorato dal browser ma mantiene viva la connessione + // quando non ci sono novita' (evita chiusure da idle-timeout dei proxy). + $heartbeat = function (): void { + echo ": keepalive\n\n"; + if (ob_get_level()) { + ob_flush(); + } + flush(); + }; + $sentDocumentIds = []; $startedAt = time(); $timeoutSeconds = 300; + $lastSignature = null; while (! connection_aborted()) { $freshDocument = OriginalDocument::query() @@ -93,6 +107,22 @@ public function stream(Request $request, OriginalDocument $originalDocument, Poc $send('document', $state->document($subDocument)); } + // Avanzamento a step per la barra di progressione della SPA: stato + // del workflow + numero di sotto-documenti gia' estratti. Si emette + // solo quando qualcosa cambia; altrimenti un heartbeat tiene viva la + // connessione senza generare rumore. + $signature = $freshDocument->processing_status->value.':'.count($sentDocumentIds); + + if ($signature !== $lastSignature) { + $send('progress', [ + 'status' => $freshDocument->processing_status->value, + 'subDocuments' => count($sentDocumentIds), + ]); + $lastSignature = $signature; + } else { + $heartbeat(); + } + if ($freshDocument->processing_status === ProcessingStatus::Completed) { $send('done', ['state' => $state->forActor($actor)]); diff --git a/apps/frontend/.angular/cache/21.2.16/poc-frontend/.tsbuildinfo b/apps/frontend/.angular/cache/21.2.16/poc-frontend/.tsbuildinfo new file mode 100644 index 0000000..2cafbf1 --- /dev/null +++ b/apps/frontend/.angular/cache/21.2.16/poc-frontend/.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["../../../../../../node_modules/typescript/lib/lib.es5.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../../../../node_modules/typescript/lib/lib.es2021.d.ts","../../../../../../node_modules/typescript/lib/lib.es2022.d.ts","../../../../../../node_modules/typescript/lib/lib.dom.d.ts","../../../../../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../../../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../../../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../../../../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../../../../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../../../../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../../../../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../../../../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../../../../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../../../../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../../../../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../../../../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../../../../../node_modules/typescript/lib/lib.decorators.d.ts","../../../../../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../../../../../node_modules/tslib/tslib.d.ts","../../../../../../node_modules/tslib/modules/index.d.ts","../../../../src/main.ngtypecheck.ts","../../../../../../node_modules/@angular/core/types/_formatter-chunk.d.ts","../../../../../../node_modules/@angular/core/types/_event_dispatcher-chunk.d.ts","../../../../../../node_modules/@angular/core/types/_chrome_dev_tools_performance-chunk.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/subscription.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/subscriber.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operator.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/types.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/audit.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/audittime.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/buffer.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/buffercount.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/buffertime.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/buffertoggle.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/bufferwhen.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/catcherror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/combinelatestall.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/combineall.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/combinelatest.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/combinelatestwith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/concat.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/concatall.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/concatmap.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/concatmapto.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/concatwith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/connect.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/count.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/debounce.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/debouncetime.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/defaultifempty.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/delay.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/delaywhen.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/dematerialize.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/distinct.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/distinctuntilchanged.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/distinctuntilkeychanged.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/elementat.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/endwith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/every.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/exhaustall.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/exhaust.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/exhaustmap.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/expand.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/filter.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/finalize.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/find.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/findindex.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/first.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/subject.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/groupby.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/ignoreelements.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/isempty.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/last.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/map.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/mapto.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/notification.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/materialize.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/max.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/merge.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/mergeall.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/mergemap.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/flatmap.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/mergemapto.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/mergescan.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/mergewith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/min.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/connectableobservable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/multicast.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/observeon.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/onerrorresumenextwith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/pairwise.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/partition.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/pluck.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/publish.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/publishbehavior.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/publishlast.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/publishreplay.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/race.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/racewith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/reduce.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/repeat.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/repeatwhen.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/retry.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/retrywhen.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/refcount.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/sample.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/sampletime.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/scan.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/sequenceequal.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/share.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/sharereplay.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/single.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/skip.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/skiplast.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/skipuntil.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/skipwhile.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/startwith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/subscribeon.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/switchall.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/switchmap.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/switchmapto.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/switchscan.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/take.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/takelast.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/takeuntil.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/takewhile.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/tap.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/throttle.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/throttletime.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/throwifempty.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/timeinterval.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/timeout.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/timeoutwith.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/timestamp.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/toarray.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/window.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/windowcount.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/windowtime.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/windowtoggle.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/windowwhen.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/withlatestfrom.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/zip.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/zipall.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/operators/zipwith.d.ts","../../../../../../node_modules/rxjs/dist/types/operators/index.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/action.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/testing/testmessage.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/testing/subscriptionlog.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/testing/subscriptionloggable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/testing/coldobservable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/testing/hotobservable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/asyncscheduler.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/timerhandle.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/asyncaction.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/virtualtimescheduler.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/testing/testscheduler.d.ts","../../../../../../node_modules/rxjs/dist/types/testing/index.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/symbol/observable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/dom/animationframes.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/behaviorsubject.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/replaysubject.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/asyncsubject.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/asapscheduler.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/asap.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/async.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/queuescheduler.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/queue.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/animationframescheduler.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduler/animationframe.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/identity.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/pipe.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/noop.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/isobservable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/lastvaluefrom.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/firstvaluefrom.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/argumentoutofrangeerror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/emptyerror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/notfounderror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/objectunsubscribederror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/sequenceerror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/util/unsubscriptionerror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/bindcallback.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/bindnodecallback.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/anycatcher.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/combinelatest.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/concat.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/connectable.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/defer.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/empty.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/forkjoin.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/from.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/fromevent.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/fromeventpattern.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/generate.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/iif.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/interval.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/merge.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/never.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/of.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/onerrorresumenext.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/pairs.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/partition.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/race.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/range.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/throwerror.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/timer.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/using.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/observable/zip.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/scheduled/scheduled.d.ts","../../../../../../node_modules/rxjs/dist/types/internal/config.d.ts","../../../../../../node_modules/rxjs/dist/types/index.d.ts","../../../../../../node_modules/@angular/core/types/_effect-chunk.d.ts","../../../../../../node_modules/@angular/core/types/primitives-di.d.ts","../../../../../../node_modules/@angular/core/types/_discovery-chunk.d.ts","../../../../../../node_modules/@angular/core/types/_api-chunk.d.ts","../../../../../../node_modules/@angular/core/types/_weak_ref-chunk.d.ts","../../../../../../node_modules/@angular/core/types/core.d.ts","../../../../../../node_modules/@angular/common/types/_platform_location-chunk.d.ts","../../../../../../node_modules/@angular/common/types/_common_module-chunk.d.ts","../../../../../../node_modules/@angular/common/types/_xhr-chunk.d.ts","../../../../../../node_modules/@angular/common/types/common.d.ts","../../../../../../node_modules/@angular/platform-browser/types/_browser-chunk.d.ts","../../../../../../node_modules/@angular/common/types/_module-chunk.d.ts","../../../../../../node_modules/@angular/common/types/http.d.ts","../../../../../../node_modules/@angular/platform-browser/types/platform-browser.d.ts","../../../../src/app/app.ngtypecheck.ts","../../../../../../node_modules/@angular/router/types/_router_module-chunk.d.ts","../../../../../../node_modules/@angular/router/types/router.d.ts","../../../../../../node_modules/@angular/core/types/rxjs-interop.d.ts","../../../../../../node_modules/@lucide/angular/types/lucide-angular.d.ts","../../../../src/app/shared/components/button/button.ngtypecheck.ts","../../../../src/app/shared/components/button/button.ts","../../../../src/app/core/navigation/app-views.ngtypecheck.ts","../../../../src/app/core/navigation/app-views.ts","../../../../src/app/core/state/poc-state.store.ngtypecheck.ts","../../../../src/api/generated/poc-api.ngtypecheck.ts","../../../../src/api/generated/model/index.ngtypecheck.ts","../../../../src/api/generated/model/assistantstate.ngtypecheck.ts","../../../../src/api/generated/model/communication.ngtypecheck.ts","../../../../src/api/generated/model/communication.ts","../../../../src/api/generated/model/metric.ngtypecheck.ts","../../../../src/api/generated/model/metric.ts","../../../../src/api/generated/model/assistantstate.ts","../../../../src/api/generated/model/copilotstate.ngtypecheck.ts","../../../../src/api/generated/model/subdocument.ngtypecheck.ts","../../../../src/api/generated/model/subdocumentreviewstatus.ngtypecheck.ts","../../../../src/api/generated/model/subdocumentreviewstatus.ts","../../../../src/api/generated/model/subdocument.ts","../../../../src/api/generated/model/copilotstate.ts","../../../../src/api/generated/model/deletedocumentresponse.ngtypecheck.ts","../../../../src/api/generated/model/pocstate.ngtypecheck.ts","../../../../src/api/generated/model/pocstate.ts","../../../../src/api/generated/model/deletedocumentresponse.ts","../../../../src/api/generated/model/errorenvelope.ngtypecheck.ts","../../../../src/api/generated/model/errorenvelopeerror.ngtypecheck.ts","../../../../src/api/generated/model/errorenvelopeerror.ts","../../../../src/api/generated/model/errorenvelope.ts","../../../../src/api/generated/model/forbiddenresponse.ngtypecheck.ts","../../../../src/api/generated/model/forbiddenresponse.ts","../../../../src/api/generated/model/generatecommunicationrequest.ngtypecheck.ts","../../../../src/api/generated/model/generatecommunicationrequeststyle.ngtypecheck.ts","../../../../src/api/generated/model/generatecommunicationrequeststyle.ts","../../../../src/api/generated/model/generatecommunicationrequesttone.ngtypecheck.ts","../../../../src/api/generated/model/generatecommunicationrequesttone.ts","../../../../src/api/generated/model/generatecommunicationrequest.ts","../../../../src/api/generated/model/generatecommunicationresponse.ngtypecheck.ts","../../../../src/api/generated/model/generatecommunicationresponse.ts","../../../../src/api/generated/model/notfoundresponse.ngtypecheck.ts","../../../../src/api/generated/model/notfoundresponse.ts","../../../../src/api/generated/model/unauthorizedresponse.ngtypecheck.ts","../../../../src/api/generated/model/unauthorizedresponse.ts","../../../../src/api/generated/model/updateextracteddatarequest.ngtypecheck.ts","../../../../src/api/generated/model/updateextracteddatarequest.ts","../../../../src/api/generated/model/updatesubdocumentreviewresponse.ngtypecheck.ts","../../../../src/api/generated/model/updatesubdocumentreviewresponse.ts","../../../../src/api/generated/model/uploaddocumentresponse.ngtypecheck.ts","../../../../src/api/generated/model/uploaddocumentresponse.ts","../../../../src/api/generated/model/uploadpocdocumentbody.ngtypecheck.ts","../../../../src/api/generated/model/uploadpocdocumentbody.ts","../../../../src/api/generated/model/upstreamunavailableresponse.ngtypecheck.ts","../../../../src/api/generated/model/upstreamunavailableresponse.ts","../../../../src/api/generated/model/validationerrorenvelope.ngtypecheck.ts","../../../../src/api/generated/model/validationerrorenvelope.ts","../../../../src/api/generated/model/validationerrorresponse.ngtypecheck.ts","../../../../src/api/generated/model/validationerrorresponse.ts","../../../../src/api/generated/model/index.ts","../../../../src/api/generated/poc-api.ts","../../../../src/app/core/errors/api-error.ngtypecheck.ts","../../../../src/app/core/errors/api-error.ts","../../../../src/app/core/state/poc-state.store.ts","../../../../src/app/core/theme/theme.service.ngtypecheck.ts","../../../../src/app/core/theme/theme.service.ts","../../../../src/app/app.ts","../../../../src/app/app.config.ngtypecheck.ts","../../../../src/app/app.routes.ngtypecheck.ts","../../../../src/app/features/overview/overview-page.ngtypecheck.ts","../../../../src/app/shared/components/empty-state/empty-state.ngtypecheck.ts","../../../../src/app/shared/components/empty-state/empty-state.ts","../../../../src/app/shared/components/error-state/error-state.ngtypecheck.ts","../../../../src/app/shared/components/alert/alert.ngtypecheck.ts","../../../../src/app/shared/components/alert/alert.ts","../../../../src/app/shared/components/error-state/error-state.ts","../../../../src/app/shared/components/metric-card/metric-card.ngtypecheck.ts","../../../../src/app/shared/components/metric-card/metric-card.ts","../../../../src/app/shared/components/metrics-panel/metrics-panel.ngtypecheck.ts","../../../../src/app/shared/components/loading-state/loading-state.ngtypecheck.ts","../../../../src/app/shared/components/loading-state/loading-state.ts","../../../../src/app/shared/components/metrics-panel/metrics-panel.ts","../../../../src/app/shared/components/status-badge/status-badge.ngtypecheck.ts","../../../../src/app/shared/util/status.ngtypecheck.ts","../../../../src/app/shared/util/status.ts","../../../../src/app/shared/components/status-badge/status-badge.ts","../../../../src/app/layout/section/section.ngtypecheck.ts","../../../../src/app/layout/section/section.ts","../../../../src/app/shared/util/formatters.ngtypecheck.ts","../../../../src/app/shared/util/formatters.ts","../../../../src/app/features/overview/overview-page.ts","../../../../src/app/features/assistant/assistant-page.ngtypecheck.ts","../../../../src/app/features/assistant/data/assistant.service.ngtypecheck.ts","../../../../src/app/features/assistant/assistant.model.ngtypecheck.ts","../../../../src/app/features/assistant/assistant.model.ts","../../../../src/app/features/assistant/data/assistant.service.ts","../../../../src/app/features/assistant/components/communication-generator-panel.ngtypecheck.ts","../../../../../../node_modules/@angular/forms/types/forms.d.ts","../../../../src/app/shared/components/select-field/select-field.ngtypecheck.ts","../../../../src/app/shared/components/select-field/select-field.ts","../../../../src/app/shared/components/textarea-field/textarea-field.ngtypecheck.ts","../../../../src/app/shared/components/textarea-field/textarea-field.ts","../../../../src/app/features/assistant/components/communication-generator-panel.ts","../../../../src/app/features/assistant/components/generated-communication-preview.ngtypecheck.ts","../../../../src/app/features/assistant/components/generated-communication-preview.ts","../../../../src/app/features/assistant/assistant-page.ts","../../../../src/app/features/copilot/copilot-page.ngtypecheck.ts","../../../../src/app/features/copilot/data/document-workflow.service.ngtypecheck.ts","../../../../src/app/features/copilot/data/document-workflow.service.ts","../../../../src/app/features/copilot/components/document-list.ngtypecheck.ts","../../../../src/app/features/copilot/components/document-list.ts","../../../../src/app/features/copilot/components/document-upload-panel.ngtypecheck.ts","../../../../src/app/shared/components/file-dropzone/file-dropzone.ngtypecheck.ts","../../../../src/app/shared/components/file-dropzone/file-dropzone.ts","../../../../src/app/features/copilot/components/upload-progress.ngtypecheck.ts","../../../../src/app/features/copilot/components/upload-progress.ts","../../../../src/app/features/copilot/components/document-upload-panel.ts","../../../../src/app/features/copilot/components/sub-document-list.ngtypecheck.ts","../../../../src/app/features/copilot/components/document-status-timeline.ngtypecheck.ts","../../../../src/app/features/copilot/components/document-status-timeline.ts","../../../../src/app/features/copilot/components/sub-document-list.ts","../../../../src/app/features/copilot/copilot-page.ts","../../../../src/app/app.routes.ts","../../../../src/app/core/interceptors/api-base-url.interceptor.ngtypecheck.ts","../../../../src/environments/environment.ngtypecheck.ts","../../../../src/environments/environment.ts","../../../../src/app/core/interceptors/api-base-url.interceptor.ts","../../../../src/app/core/interceptors/correlation.interceptor.ngtypecheck.ts","../../../../src/app/core/observability/correlation.ngtypecheck.ts","../../../../src/app/core/observability/correlation.ts","../../../../src/app/core/interceptors/correlation.interceptor.ts","../../../../src/app/core/interceptors/error-logging.interceptor.ngtypecheck.ts","../../../../src/app/core/observability/logger.service.ngtypecheck.ts","../../../../src/app/core/observability/logger.service.ts","../../../../src/app/core/interceptors/error-logging.interceptor.ts","../../../../src/app/core/observability/global-error-handler.ngtypecheck.ts","../../../../src/app/core/observability/global-error-handler.ts","../../../../src/app/app.config.ts","../../../../src/main.ts","../../../../src/environments/environment.development.ngtypecheck.ts","../../../../src/environments/environment.development.ts"],"fileIdsList":[[61],[61,281,283,285],[61,282],[61,285,287,291],[61,293,295],[61,297,299],[61,298],[61,300,301],[61,303,305,307],[61,304],[61,306],[61,283,295,309],[61,280,283,285,286,290,291,292,295,296,299,300,302,305,307,308,310,312,314,316,318,320,322,324,326,328],[61,284],[61,300,311],[61,286,292,294],[61,288,290],[61,289],[61,300,313],[61,315],[61,291,295,317],[61,319],[61,321],[61,300,323],[61,300,325],[61,326,327],[61,254,260,267,279,329],[61,260,267,271,337,392,396,400,404,406],[61,271,277,338,360,375,391],[61,254,260,269,271,272,273,275,277,333,335],[61,267,331],[61,267,393,395],[61,267,397,399],[61,254,260,267,332,399,401,403],[61,276],[61,398],[61,260,267,403,405],[61,260,402],[61,254,260,278,329,330,332],[61,260,334],[61,254,260,329,332,333,341,345,355,357,359,361,364,365,372,374],[61,329,363],[61,260,273,275,329,357,364,366,367,369,371],[61,260,341,355,357,364,373],[61,254,260,329,330,333,362,364],[61,260,275,329,341,354,355,359,379],[61,260,329,354,355,388],[61,260,357,378,381,383,385],[61,260,268,273,275,329,341,357,359,367,378,387,389],[61,260,378,384],[61,254,260,329,332,333,345,351,357,376,378,380,386,390],[61,254,260,329,330,333,359,377],[61,260,271,275,277,333,339,341,345,347,351,355,357,359],[61,260,356],[61,260,343],[61,260,274],[61,260,340],[61,260,342,344],[61,260,273,382],[61,260,349],[61,260,346],[61,260,329,341,347,348,350],[61,260,367,368],[61,260,352,354],[61,260,367,370],[61,358],[61,353],[61,409],[61,394],[61,62,268,336,407],[254,260,261],[260],[254,260,261,262,263],[254,260,263,266],[65],[63,64,260],[63,64,65,254,255,256,260],[63],[63,64,65,254,255,256,257,258,259,260],[63,64,65,254,258,260],[254,260],[260,264],[260,264,265,267],[254,260,264],[254,260,264,268,270],[260,273],[66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,82,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,122,123,124,125,126,127,128,129,130,131,132,133,135,136,137,138,139,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,185,186,187,189,198,200,201,202,203,204,205,207,208,210,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253],[111],[67,70],[69],[69,70],[66,67,68,70],[67,69,70,227],[70],[66,69,111],[69,70,227],[69,235],[67,69,70],[79],[102],[123],[69,70,111],[70,118],[69,70,111,129],[69,70,129],[70,170],[70,111],[66,70,188],[66,70,189],[211],[195,197],[206],[195],[66,70,188,195,196],[188,189,197],[209],[66,70,195,196,197],[68,69,70],[66,70],[67,69,189,190,191,192],[111,189,190,191,192],[189,191],[69,190,191,193,194,198],[66,69],[70,213],[71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,112,113,114,115,116,117,119,120,121,122,123,124,125,126,127,128,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186],[199],[60]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6a5253138c5432c68a1510c70fe78a644fe2e632111ba778e1978010d6edfec","impliedFormat":1},{"version":"b8f34dd1757f68e03262b1ca3ddfa668a855b872f8bdd5224d6f993a7b37dc2c","impliedFormat":99},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"c8f8b8f9f173829b20e0897287ae4d89bbccb725a05d89560c9cd4f82b987a50","affectsGlobalScope":true,"impliedFormat":99},{"version":"c061611ae84390e04fb54e0b0e4f3585d08f8e47c32d0388b83b3ba8f9e08d63","affectsGlobalScope":true,"impliedFormat":99},{"version":"a7479bf13c58e5bb53920548956aea53ac3e3818139727564d8a263d0daa50c9","affectsGlobalScope":true,"impliedFormat":99},{"version":"073ca26c96184db9941b5ec0ddea6981c9b816156d9095747809e524fdd90e35","impliedFormat":1},{"version":"e41d17a2ec23306d953cda34e573ed62954ca6ea9b8c8b74e013d07a6886ce47","impliedFormat":1},{"version":"241bd4add06f06f0699dcd58f3b334718d85e3045d9e9d4fa556f11f4d1569c1","impliedFormat":1},{"version":"2ae3787e1498b20aad1b9c2ee9ea517ec30e89b70d242d8e3e52d1e091039695","impliedFormat":1},{"version":"c7c72c4cffb1bc83617eefed71ed68cc89df73cab9e19507ccdecb3e72b4967e","affectsGlobalScope":true,"impliedFormat":1},{"version":"b8bff8a60af0173430b18d9c3e5c443eaa3c515617210c0c7b3d2e1743c19ecb","impliedFormat":1},{"version":"38b38db08e7121828294dec10957a7a9ff263e33e2a904b346516d4a4acca482","impliedFormat":1},{"version":"a76ebdf2579e68e4cfe618269c47e5a12a4e045c2805ed7f7ab37af8daa6b091","impliedFormat":1},{"version":"8a2aaea564939c22be05d665cc955996721bad6d43148f8fa21ae8f64afecd37","impliedFormat":1},{"version":"e59d36b7b6e8ba2dd36d032a5f5c279d2460968c8b4e691ca384f118fb09b52a","impliedFormat":1},{"version":"e96885c0684c9042ec72a9a43ef977f6b4b4a2728f4b9e737edcbaa0c74e5bf6","impliedFormat":1},{"version":"95950a187596e206d32d5d9c7b932901088c65ed8f9040e614aa8e321e0225ef","impliedFormat":1},{"version":"89e061244da3fc21b7330f4bd32f47c1813dd4d7f1dc3d0883d88943f035b993","impliedFormat":1},{"version":"e46558c2e04d06207b080138678020448e7fc201f3d69c2601b0d1456105f29a","impliedFormat":1},{"version":"71549375db52b1163411dba383b5f4618bdf35dc57fa327a1c7d135cf9bf67d1","impliedFormat":1},{"version":"7e6b2d61d6215a4e82ea75bc31a80ebb8ad0c2b37a60c10c70dd671e8d9d6d5d","impliedFormat":1},{"version":"78bea05df2896083cca28ed75784dde46d4b194984e8fc559123b56873580a23","impliedFormat":1},{"version":"5dd04ced37b7ea09f29d277db11f160df7fd73ba8b9dba86cb25552e0653a637","impliedFormat":1},{"version":"f74b81712e06605677ae1f061600201c425430151f95b5ef4d04387ad7617e6a","impliedFormat":1},{"version":"9a72847fcf4ac937e352d40810f7b7aec7422d9178451148296cf1aa19467620","impliedFormat":1},{"version":"3ae18f60e0b96fa1e025059b7d25b3247ba4dcb5f4372f6d6e67ce2adac74eac","impliedFormat":1},{"version":"2b9260f44a2e071450ae82c110f5dc8f330c9e5c3e85567ed97248330f2bf639","impliedFormat":1},{"version":"4f196e13684186bda6f5115fc4677a87cf84a0c9c4fc17b8f51e0984f3697b6d","impliedFormat":1},{"version":"61419f2c5822b28c1ea483258437c1faab87d00c6f84481aa22afb3380d8e9a4","impliedFormat":1},{"version":"64479aee03812264e421c0bf5104a953ca7b02740ba80090aead1330d0effe91","impliedFormat":1},{"version":"0521108c9f8ddb17654a0a54dae6ba9667c99eddccfd6af5748113e022d1c37a","impliedFormat":1},{"version":"c5570e504be103e255d80c60b56c367bf45d502ca52ee35c55dec882f6563b5c","impliedFormat":1},{"version":"ee764e6e9a7f2b987cc1a2c0a9afd7a8f4d5ebc4fdb66ad557a7f14a8c2bd320","impliedFormat":1},{"version":"0520b5093712c10c6ef23b5fea2f833bf5481771977112500045e5ea7e8e2b69","impliedFormat":1},{"version":"5c3cf26654cf762ac4d7fd7b83f09acfe08eef88d2d6983b9a5a423cb4004ca3","impliedFormat":1},{"version":"e60fa19cf7911c1623b891155d7eb6b7e844e9afdf5738e3b46f3b687730a2bd","impliedFormat":1},{"version":"b1fd72ff2bb0ba91bb588f3e5329f8fc884eb859794f1c4657a2bfa122ae54d0","impliedFormat":1},{"version":"6cf42a4f3cfec648545925d43afaa8bb364ac10a839ffed88249da109361b275","impliedFormat":1},{"version":"d7058e75920120b142a9d57be25562a3cd9a936269fd52908505f530105f2ec4","impliedFormat":1},{"version":"6df52b70d7f7702202f672541a5f4a424d478ee5be51a9d37b8ccbe1dbf3c0f2","impliedFormat":1},{"version":"0ca7f997e9a4d8985e842b7c882e521b6f63233c4086e9fe79dd7a9dc4742b5e","impliedFormat":1},{"version":"91046b5c6b55d3b194c81fd4df52f687736fad3095e9d103ead92bb64dc160ee","impliedFormat":1},{"version":"db5704fdad56c74dfc5941283c1182ed471bd17598209d3ac4a49faa72e43cfc","impliedFormat":1},{"version":"758e8e89559b02b81bc0f8fd395b17ad5aff75490c862cbe369bb1a3d1577c40","impliedFormat":1},{"version":"2ee64342c077b1868f1834c063f575063051edd6e2964257d34aad032d6b657c","impliedFormat":1},{"version":"6f6b4b3d670b6a5f0e24ea001c1b3d36453c539195e875687950a178f1730fa7","impliedFormat":1},{"version":"a472a1d3f25ce13a1d44911cd3983956ac040ce2018e155435ea34afb25f864c","impliedFormat":1},{"version":"b48b83a86dd9cfe36f8776b3ff52fcd45b0e043c0538dc4a4b149ba45fe367b9","impliedFormat":1},{"version":"792de5c062444bd2ee0413fb766e57e03cce7cdaebbfc52fc0c7c8e95069c96b","impliedFormat":1},{"version":"a79e3e81094c7a04a885bad9b049c519aace53300fb8a0fe4f26727cb5a746ce","impliedFormat":1},{"version":"93181bac0d90db185bb730c95214f6118ae997fe836a98a49664147fbcaf1988","impliedFormat":1},{"version":"8a4e89564d8ea66ad87ee3762e07540f9f0656a62043c910d819b4746fc429c5","impliedFormat":1},{"version":"b9011d99942889a0f95e120d06b698c628b0b6fdc3e6b7ecb459b97ed7d5bcc6","impliedFormat":1},{"version":"4d639cbbcc2f8f9ce6d55d5d503830d6c2556251df332dc5255d75af53c8a0e7","impliedFormat":1},{"version":"cdb48277f600ab5f429ecf1c5ea046683bc6b9f73f3deab9a100adac4b34969c","impliedFormat":1},{"version":"75be84956a29040a1afbe864c0a7a369dfdb739380072484eff153905ef867ee","impliedFormat":1},{"version":"b06b4adc2ae03331a92abd1b19af8eb91ec2bf8541747ee355887a167d53145e","impliedFormat":1},{"version":"c54166a85bd60f86d1ebb90ce0117c0ecb850b8a33b366691629fdf26f1bbbd8","impliedFormat":1},{"version":"0d417c15c5c635384d5f1819cc253a540fe786cc3fda32f6a2ae266671506a21","impliedFormat":1},{"version":"80f23f1d60fbed356f726b3b26f9d348dddbb34027926d10d59fad961e70a730","impliedFormat":1},{"version":"cb59317243a11379a101eb2f27b9df1022674c3df1df0727360a0a3f963f523b","impliedFormat":1},{"version":"cc20bb2227dd5de0aab0c8d697d1572f8000550e62c7bf5c92f212f657dd88c5","impliedFormat":1},{"version":"06b8a7d46195b6b3980e523ef59746702fd210b71681a83a5cf73799623621f9","impliedFormat":1},{"version":"860e4405959f646c101b8005a191298b2381af8f33716dc5f42097e4620608f8","impliedFormat":1},{"version":"f7e32adf714b8f25d3c1783473abec3f2e82d5724538d8dcf6f51baaaff1ca7a","impliedFormat":1},{"version":"d0da80c845999a16c24d0783033fb5366ada98df17867c98ad433ede05cd87fd","impliedFormat":1},{"version":"bfbf80f9cd4558af2d7b2006065340aaaced15947d590045253ded50aabb9bc5","impliedFormat":1},{"version":"fd9a991b51870325e46ebb0e6e18722d313f60cd8e596e645ec5ac15b96dbf4e","impliedFormat":1},{"version":"c3bd2b94e4298f81743d92945b80e9b56c1cdfb2bef43c149b7106a2491b1fc9","impliedFormat":1},{"version":"a246cce57f558f9ebaffd55c1e5673da44ea603b4da3b2b47eb88915d30a9181","impliedFormat":1},{"version":"d993eacc103c5a065227153c9aae8acea3a4322fe1a169ee7c70b77015bf0bb2","impliedFormat":1},{"version":"fc2b03d0c042aa1627406e753a26a1eaad01b3c496510a78016822ef8d456bb6","impliedFormat":1},{"version":"063c7ebbe756f0155a8b453f410ca6b76ffa1bbc1048735bcaf9c7c81a1ce35f","impliedFormat":1},{"version":"314e402cd481370d08f63051ae8b8c8e6370db5ee3b8820eeeaaf8d722a6dac6","impliedFormat":1},{"version":"9669075ac38ce36b638b290ba468233980d9f38bdc62f0519213b2fd3e2552ec","impliedFormat":1},{"version":"4d123de012c24e2f373925100be73d50517ac490f9ed3578ac82d0168bfbd303","impliedFormat":1},{"version":"656c9af789629aa36b39092bee3757034009620439d9a39912f587538033ce28","impliedFormat":1},{"version":"3ac3f4bdb8c0905d4c3035d6f7fb20118c21e8a17bee46d3735195b0c2a9f39f","impliedFormat":1},{"version":"1f453e6798ed29c86f703e9b41662640d4f2e61337007f27ac1c616f20093f69","impliedFormat":1},{"version":"af43b7871ff21c62bf1a54ec5c488e31a8d3408d5b51ff2e9f8581b6c55f2fc7","impliedFormat":1},{"version":"70550511d25cbb0b6a64dcac7fffc3c1397fd4cbeb6b23ccc7f9b794ab8a6954","impliedFormat":1},{"version":"af0fbf08386603a62f2a78c42d998c90353b1f1d22e05a384545f7accf881e0a","impliedFormat":1},{"version":"cefc20054d20b85b534206dbcedd509bb74f87f3d8bc45c58c7be3a76caa45e1","impliedFormat":1},{"version":"ad6eee4877d0f7e5244d34bc5026fd6e9cf8e66c5c79416b73f9f6ebf132f924","impliedFormat":1},{"version":"4888fd2bcfee9a0ce89d0df860d233e0cee8ee9c479b6bd5a5d5f9aae98342fe","impliedFormat":1},{"version":"f4749c102ced952aa6f40f0b579865429c4869f6d83df91000e98005476bee87","impliedFormat":1},{"version":"56654d2c5923598384e71cb808fac2818ca3f07dd23bb018988a39d5e64f268b","impliedFormat":1},{"version":"8b6719d3b9e65863da5390cb26994602c10a315aa16e7d70778a63fee6c4c079","impliedFormat":1},{"version":"05f56cd4b929977d18df8f3d08a4c929a2592ef5af083e79974b20a063f30940","impliedFormat":1},{"version":"547d3c406a21b30e2b78629ecc0b2ddaf652d9e0bdb2d59ceebce5612906df33","impliedFormat":1},{"version":"b3a4f9385279443c3a5568ec914a9492b59a723386161fd5ef0619d9f8982f97","impliedFormat":1},{"version":"3fe66aba4fbe0c3ba196a4f9ed2a776fe99dc4d1567a558fb11693e9fcc4e6ed","impliedFormat":1},{"version":"140eef237c7db06fc5adcb5df434ee21e81ee3a6fd57e1a75b8b3750aa2df2d8","impliedFormat":1},{"version":"0944ec553e4744efae790c68807a461720cff9f3977d4911ac0d918a17c9dd99","impliedFormat":1},{"version":"cb46b38d5e791acaa243bf342b8b5f8491639847463ac965b93896d4fb0af0d9","impliedFormat":1},{"version":"7c7d9e116fe51100ff766703e6b5e4424f51ad8977fe474ddd8d0959aa6de257","impliedFormat":1},{"version":"af70a2567e586be0083df3938b6a6792e6821363d8ef559ad8d721a33a5bcdaf","impliedFormat":1},{"version":"006cff3a8bcb92d77953f49a94cd7d5272fef4ab488b9052ef82b6a1260d870b","impliedFormat":1},{"version":"7d44bfdc8ee5e9af70738ff652c622ae3ad81815e63ab49bdc593d34cb3a68e5","impliedFormat":1},{"version":"339814517abd4dbc7b5f013dfd3b5e37ef0ea914a8bbe65413ecffd668792bc6","impliedFormat":1},{"version":"34d5bc0a6958967ec237c99f980155b5145b76e6eb927c9ffc57d8680326b5d8","impliedFormat":1},{"version":"9eae79b70c9d8288032cbe1b21d0941f6bd4f315e14786b2c1d10bccc634e897","impliedFormat":1},{"version":"18ce015ed308ea469b13b17f99ce53bbb97975855b2a09b86c052eefa4aa013a","impliedFormat":1},{"version":"5a931bc4106194e474be141e0bc1046629510dc95b9a0e4b02a3783847222965","impliedFormat":1},{"version":"5e5f371bf23d5ced2212a5ff56675aefbd0c9b3f4d4fdda1b6123ac6e28f058c","impliedFormat":1},{"version":"907c17ad5a05eecb29b42b36cc8fec6437be27cc4986bb3a218e4f74f606911c","impliedFormat":1},{"version":"ce60a562cd2a92f37a88f2ddd99a3abfbc5848d7baf38c48fb8d3243701fcb75","impliedFormat":1},{"version":"a726ad2d0a98bfffbe8bc1cd2d90b6d831638c0adc750ce73103a471eb9a891c","impliedFormat":1},{"version":"f44c0c8ce58d3dacac016607a1a90e5342d830ea84c48d2e571408087ae55894","impliedFormat":1},{"version":"75a315a098e630e734d9bc932d9841b64b30f7a349a20cf4717bf93044eff113","impliedFormat":1},{"version":"9131d95e32b3d4611d4046a613e022637348f6cebfe68230d4e81b691e4761a1","impliedFormat":1},{"version":"b03aa292cfdcd4edc3af00a7dbd71136dd067ec70a7536b655b82f4dd444e857","impliedFormat":1},{"version":"b6e2b0448ced813b8c207810d96551a26e7d7bb73255eea4b9701698f78846d6","impliedFormat":1},{"version":"8ae10cd85c1bd94d2f2d17c4cbd25c068a4b2471c70c2d96434239f97040747a","impliedFormat":1},{"version":"9ed5b799c50467b0c9f81ddf544b6bcda3e34d92076d6cab183c84511e45c39f","impliedFormat":1},{"version":"b4fa87cc1833839e51c49f20de71230e259c15b2c9c3e89e4814acc1d1ef10de","impliedFormat":1},{"version":"e90ac9e4ac0326faa1bc39f37af38ace0f9d4a655cd6d147713c653139cf4928","impliedFormat":1},{"version":"ea27110249d12e072956473a86fd1965df8e1be985f3b686b4e277afefdde584","impliedFormat":1},{"version":"8776a368617ce51129b74db7d55c3373dadcce5d0701e61d106e99998922a239","impliedFormat":1},{"version":"5666075052877fe2fdddd5b16de03168076cf0f03fbca5c1d4a3b8f43cba570c","impliedFormat":1},{"version":"9108ab5af05418f599ab48186193b1b07034c79a4a212a7f73535903ba4ca249","impliedFormat":1},{"version":"bb4e2cdcadf9c9e6ee2820af23cee6582d47c9c9c13b0dca1baaffe01fbbcb5f","impliedFormat":1},{"version":"6e30d0b5a1441d831d19fe02300ab3d83726abd5141cbcc0e2993fa0efd33db4","impliedFormat":1},{"version":"423f28126b2fc8d8d6fa558035309000a1297ed24473c595b7dec52e5c7ebae5","impliedFormat":1},{"version":"fb30734f82083d4790775dae393cd004924ebcbfde49849d9430bf0f0229dd16","impliedFormat":1},{"version":"2c92b04a7a4a1cd9501e1be338bf435738964130fb2ad5bd6c339ee41224ac4c","impliedFormat":1},{"version":"c5c5f0157b41833180419dacfbd2bcce78fb1a51c136bd4bcba5249864d8b9b5","impliedFormat":1},{"version":"02ae43d5bae42efcd5a00d3923e764895ce056bca005a9f4e623aa6b4797c8af","impliedFormat":1},{"version":"db6e01f17012a9d7b610ae764f94a1af850f5d98c9c826ad61747dca0fb800bd","impliedFormat":1},{"version":"8a44b424edee7bb17dc35a558cc15f92555f14a0441205613e0e50452ab3a602","impliedFormat":1},{"version":"24a00d0f98b799e6f628373249ece352b328089c3383b5606214357e9107e7d5","impliedFormat":1},{"version":"33637e3bc64edd2075d4071c55d60b32bdb0d243652977c66c964021b6fc8066","impliedFormat":1},{"version":"0f0ad9f14dedfdca37260931fac1edf0f6b951c629e84027255512f06a6ebc4c","impliedFormat":1},{"version":"16ad86c48bf950f5a480dc812b64225ca4a071827d3d18ffc5ec1ae176399e36","impliedFormat":1},{"version":"8cbf55a11ff59fd2b8e39a4aa08e25c5ddce46e3af0ed71fb51610607a13c505","impliedFormat":1},{"version":"d5bc4544938741f5daf8f3a339bfbf0d880da9e89e79f44a6383aaf056fe0159","impliedFormat":1},{"version":"97f9169882d393e6f303f570168ca86b5fe9aab556e9a43672dae7e6bb8e6495","impliedFormat":1},{"version":"7c9adb3fcd7851497818120b7e151465406e711d6a596a71b807f3a17853cb58","impliedFormat":1},{"version":"6752d402f9282dd6f6317c8c048aaaac27295739a166eed27e00391b358fed9a","impliedFormat":1},{"version":"9fd7466b77020847dbc9d2165829796bf7ea00895b2520ff3752ffdcff53564b","impliedFormat":1},{"version":"fbfc12d54a4488c2eb166ed63bab0fb34413e97069af273210cf39da5280c8d6","impliedFormat":1},{"version":"85a84240002b7cf577cec637167f0383409d086e3c4443852ca248fc6e16711e","impliedFormat":1},{"version":"84794e3abd045880e0fadcf062b648faf982aa80cfc56d28d80120e298178626","impliedFormat":1},{"version":"053d8b827286a16a669a36ffc8ccc8acdf8cc154c096610aa12348b8c493c7b8","impliedFormat":1},{"version":"3cce4ce031710970fe12d4f7834375f5fd455aa129af4c11eb787935923ff551","impliedFormat":1},{"version":"8f62cbd3afbd6a07bb8c934294b6bfbe437021b89e53a4da7de2648ecfc7af25","impliedFormat":1},{"version":"62c3621d34fb2567c17a2c4b89914ebefbfbd1b1b875b070391a7d4f722e55dc","impliedFormat":1},{"version":"c05ac811542e0b59cb9c2e8f60e983461f0b0e39cea93e320fad447ff8e474f3","impliedFormat":1},{"version":"8e7a5b8f867b99cc8763c0b024068fb58e09f7da2c4810c12833e1ca6eb11c4f","impliedFormat":1},{"version":"132351cbd8437a463757d3510258d0fa98fd3ebef336f56d6f359cf3e177a3ce","impliedFormat":1},{"version":"df877050b04c29b9f8409aa10278d586825f511f0841d1ec41b6554f8362092b","impliedFormat":1},{"version":"33d1888c3c27d3180b7fd20bac84e97ecad94b49830d5dd306f9e770213027d1","impliedFormat":1},{"version":"ee942c58036a0de88505ffd7c129f86125b783888288c2389330168677d6347f","impliedFormat":1},{"version":"a3f317d500c30ea56d41501632cdcc376dae6d24770563a5e59c039e1c2a08ec","impliedFormat":1},{"version":"eb21ddc3a8136a12e69176531197def71dc28ffaf357b74d4bf83407bd845991","impliedFormat":1},{"version":"0c1651a159995dfa784c57b4ea9944f16bdf8d924ed2d8b3db5c25d25749a343","impliedFormat":1},{"version":"aaa13958e03409d72e179b5d7f6ec5c6cc666b7be14773ae7b6b5ee4921e52db","impliedFormat":1},{"version":"0a86e049843ad02977a94bb9cdfec287a6c5a0a4b6b5391a6648b1a122072c5a","impliedFormat":1},{"version":"40f06693e2e3e58526b713c937895c02e113552dc8ba81ecd49cdd9596567ddb","impliedFormat":1},{"version":"4ed5e1992aedb174fb8f5aa8796aa6d4dcb8bd819b4af1b162a222b680a37fa0","impliedFormat":1},{"version":"d7f4bd46a8b97232ea6f8c28012b8d2b995e55e729d11405f159d3e00c51420a","impliedFormat":1},{"version":"d604d413aff031f4bfbdae1560e54ebf503d374464d76d50a2c6ded4df525712","impliedFormat":1},{"version":"e4f4f9cf1e3ac9fd91ada072e4d428ecbf0aa6dc57138fb797b8a0ca3a1d521c","impliedFormat":1},{"version":"12bfd290936824373edda13f48a4094adee93239b9a73432db603127881a300d","impliedFormat":1},{"version":"340ceb3ea308f8e98264988a663640e567c553b8d6dc7d5e43a8f3b64f780374","impliedFormat":1},{"version":"c5a769564e530fba3ec696d0a5cff1709b9095a0bdf5b0826d940d2fc9786413","impliedFormat":1},{"version":"7124ef724c3fc833a17896f2d994c368230a8d4b235baed39aa8037db31de54f","impliedFormat":1},{"version":"5de1c0759a76e7710f76899dcae601386424eab11fb2efaf190f2b0f09c3d3d3","impliedFormat":1},{"version":"9c5ee8f7e581f045b6be979f062a61bf076d362bf89c7f966b993a23424e8b0d","impliedFormat":1},{"version":"1a11df987948a86aa1ec4867907c59bdf431f13ed2270444bf47f788a5c7f92d","impliedFormat":1},{"version":"8018dd2e95e7ce6e613ddd81672a54532614dc745520a2f9e3860ff7fb1be0ca","impliedFormat":1},{"version":"b756781cd40d465da57d1fc6a442c34ae61fe8c802d752aace24f6a43fedacee","impliedFormat":1},{"version":"0fe76167c87289ea094e01616dcbab795c11b56bad23e1ef8aba9aa37e93432a","impliedFormat":1},{"version":"3a45029dba46b1f091e8dc4d784e7be970e209cd7d4ff02bd15270a98a9ba24b","impliedFormat":1},{"version":"032c1581f921f8874cf42966f27fd04afcabbb7878fa708a8251cac5415a2a06","impliedFormat":1},{"version":"69c68ed9652842ce4b8e495d63d2cd425862104c9fb7661f72e7aa8a9ef836f8","impliedFormat":1},{"version":"0e704ee6e9fd8b6a5a7167886f4d8915f4bc22ed79f19cb7b32bd28458f50643","impliedFormat":1},{"version":"06f62a14599a68bcde148d1efd60c2e52e8fa540cc7dcfa4477af132bb3de271","impliedFormat":1},{"version":"904a96f84b1bcee9a7f0f258d17f8692e6652a0390566515fe6741a5c6db8c1c","impliedFormat":1},{"version":"11f19ce32d21222419cecab448fa335017ebebf4f9e5457c4fa9df42fa2dcca7","impliedFormat":1},{"version":"2e8ee2cbb5e9159764e2189cf5547aebd0e6b0d9a64d479397bb051cd1991744","impliedFormat":1},{"version":"1b0471d75f5adb7f545c1a97c02a0f825851b95fe6e069ac6ecaa461b8bb321d","impliedFormat":1},{"version":"1d157c31a02b1e5cca9bc495b3d8d39f4b42b409da79f863fb953fbe3c7d4884","impliedFormat":1},{"version":"07baaceaec03d88a4b78cb0651b25f1ae0322ac1aa0b555ae3749a79a41cba86","impliedFormat":1},{"version":"619a132f634b4ebe5b4b4179ea5870f62f2cb09916a25957bff17b408de8b56d","impliedFormat":1},{"version":"f60fa446a397eb1aead9c4e568faf2df8068b4d0306ebc075fb4be16ed26b741","impliedFormat":1},{"version":"f3cb784be4d9e91f966a0b5052a098d9b53b0af0d341f690585b0cc05c6ca412","impliedFormat":1},{"version":"350f63439f8fe2e06c97368ddc7fb6d6c676d54f59520966f7dbbe6a4586014e","impliedFormat":1},{"version":"eba613b9b357ac8c50a925fa31dc7e65ff3b95a07efbaa684b624f143d8d34ba","impliedFormat":1},{"version":"45b74185005ed45bec3f07cac6e4d68eaf02ead9ff5a66721679fb28020e5e7c","impliedFormat":1},{"version":"0f6199602df09bdb12b95b5434f5d7474b1490d2cd8cc036364ab3ba6fd24263","impliedFormat":1},{"version":"c8ca7fd9ec7a3ec82185bfc8213e4a7f63ae748fd6fced931741d23ef4ea3c0f","impliedFormat":1},{"version":"5c6a8a3c2a8d059f0592d4eab59b062210a1c871117968b10797dee36d991ef7","impliedFormat":1},{"version":"ad77fd25ece8e09247040826a777dc181f974d28257c9cd5acb4921b51967bd8","impliedFormat":1},{"version":"b4c30d6cfbf0d68391c52669b850f3a88702ac503310c8fd2797abed68d2e3e8","impliedFormat":99},{"version":"f2d924a97039db1c9a899a22968e7f0633ff3b277010eef5b68c6fe41e19c868","impliedFormat":99},{"version":"444eea06239b4e550abc6a6e499c32908e290e0ba3e3e6e61b9c59947ebbe2a0","impliedFormat":99},{"version":"dbe97c7b5193445f9aef6bd9c40fe2a77328b9a6490c37c78698f277e3e9c503","impliedFormat":99},{"version":"99df1e9de518818baeb854fa83f4c80070bf1379e709081ee5fb8b6466db6110","impliedFormat":99},{"version":"b3759b2152005063df24ca4b68170c782207b56c308c1d021bdab787d387000f","impliedFormat":99},{"version":"62c552dd6b0028b299178d32048d3512399aa72437e6be3f1299edb5652765de","impliedFormat":99},{"version":"eb585fc9d4ae13d9990aa8002cee4f93f2f42119218e808bd083e2296fad64f0","impliedFormat":99},{"version":"91dd34357e0ad586e556dc8090b636ec6ddfcc14749f618a9c9d70623ab11820","impliedFormat":99},{"version":"8baeea16812cb1698bfa5aa07aa8750732954bd7d32cb334b1eff3992a809e62","impliedFormat":99},{"version":"330edd0a70fda0639d49ea30c1cf751c381501999f2b4fe563149bdfa0eef9a1","impliedFormat":99},{"version":"17762c5d17f5018dd008c4541236c4f3c7a980e17202957203b2e504e9ab3e5e","impliedFormat":99},{"version":"b4de307a7fe106f17e76c97a4809899966eb14c980f59891b99f3b0bc06cd2a0","impliedFormat":99},{"version":"628bf73417df693e07a76ab8dd70949eb02e75c8db9dae836ad7958710bd1947","impliedFormat":99},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"a688bce5f3c34404dd3151a75487505ba04e1df0488140f8b5c8b86cae83323a","impliedFormat":99},{"version":"4db585f342e1a8182941fe29394bded8cf98352913bb2242f7cbc84f17645aba","impliedFormat":99},{"version":"f19dd01efc3b0b1fa3978353c4a1b415b969bbf88c0993e9a374ef165afa5e70","impliedFormat":99},{"version":"4d727bf138cad31af92dd2ecce73473391de3955d538cf57bdc99674bb6582e9","impliedFormat":1},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","3d72cbdd175d6b02a78edd170e2caabd4e4b17a60c3f0a386a35efce292c1ee8","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","8fae756e14f5ec33970718481234c26e8828f5acb44f16e15b5d64f18ebfc70e","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","3b439d2d429f1b99361c2d5bc14226097d8da269c303204d245a7218c43aa33b","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","4fa8ea9b35f329f2e6f58fff7d34843dbe5847f31e287b3ebb53c82a71863e5d","37a8791d82c199b906d05d987c607974b794af2ac89c54482f1a7b503545a5c4","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","d4c6e70c2a68abc75055bf11a70007464573bcb8d28e9f6288dacefd6b56c30d","9402badf367108d15ce5a76711a4908e31c89bd0523b756ce7a9e638050ea49d","054439a35b9418d4993d316180f54e63e51e46300b7038a0e8527118defca4f4","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","88a604283e165cbd3ca40a4a6c0edb1fa5ebe9a1d6d4acc50b7503a061c9d841","a9ed4d88b62a0c746ab782fb9b48d25793aa6b130be3d5fa60d94c3033bd85f3","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","6ca46c67782868b7c5e0802012903d15e819374304361106636640e0c1aba39f","a98d9c8f8d5b66dc70f25ee842c9f3b306d8fbf2c948a41fbed7c174fda639fe","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","b41b47612dfea42b7bb5636e60fecfbcaebf5215bb527116535502038751a2ee","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","529d83bd67dae400dc3ecb7933d57515311459aba8175a44cb49392db399ef1e","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","59218062b9c70f0091c7b953ec6a5dab6a45c09f3aeddf3d34b5a210d27d6471","ab5e9f9119c75c93319835115980ea0fec8dd212c01a5a127978041f1a30503a","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","7f181324a10ee2ab9b0a4f97459e078dcb34b7e3abf63a41850151b99476f379","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","4d573c8bfb03f7c388264c6523f7e2b6bbb149527cf47d1f9bf7f9bbc9f430a8","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","767c188a33db8d3e4197fb9304293bf5b72988b2f7fd1a9a50adb0d635467d7f","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","e7fb9b4dd032d06e656eb33247b8a368e7e8a4047a02ab8d26de37fb0e6cbbf6","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","cf263b7bdbfe2a4edef7016f8cd3b4a0627b76971e016d5fd81b90f040969d82","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","dce50ef057f814198f7e1c3157d1dc240bc9aa343e3fcdec1fb607cf81882002","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","a79e06bd7bedd9f93d2dec6ba4c995e7a7fa14405a6e95d5f99f4cc2030ce83c","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","f157bfc3d28054dc724eedb9d5b384435653bcbda1426bea88b8b2d7d2a6e4f7","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","b74e51ad634bb012c83ba29c2f98d0213beb950fab4d8d330f704eb5a73609db","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","366e906d9847dc8ca665e67c8e1be11de44b19895bad517992436bdecbee8b27","e0201db28648521fa725d8a12aa9fdfd237a4d1ef3a2db1ab2cb352ea44df7ac","c1b217d80065d77b241713cefdf95bdcaef8f88fc1649273503ba9524cdcae71","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","080a2f121316b26fa4003765ccb261415af2938c728a6ed06d73893971ae1222","ed6001c05abb4af2fd22ee54d7b34b69cf98d5a2dc96a6397bfc7f0c7f3774ba","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ba32d0a36d786a4091ed2136d3fe4d37492fad09b86e8505c8833ac01da4a1a5",{"version":"a58a595baaadb43c9eb5c041ff25272ff72821126e6074917c75cce4525703cd","signature":"af687e3b8d1d4afaec4643e191f1d0ba63344981f88f41f1725c8ee75623c317"},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","dd1f48fa47d8bd052687fbaf478df438597a9d05456a2d288e186538a096800c","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","1e1386d04ab5c9ce2ecb85822a2c532a862b61b5bf17a95bb24056583a302db8","ddcd5fc57e3e506784bda51d62d334fe10639a5191869489bc18a42e3853931b","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","e7a281281bd6b979367f699c62375933353927a65473e45181ef88e1798b2407","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","a05fe6bc903d809d85bb5a896afd6af1aab1d069f9f12ccf5e671da19d6a146c","35395b038fe0bf23348a320d52d8e1aeee42bcf2df7b84b0fcdfce82d5df9614","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"f1f862c2256b3c3db3222c5cc74866f0865ffb4a63982dc5f689a2d58f23f609","signature":"0b1cfd9689673da1abc2c14c13839f73a99c5f3759302c85f7c263da4f151880"},{"version":"7d6832c6c00e6c2c17ed827f0d528b8587ea3857ff7236a357c698a1a6e401f8","signature":"7f38a5f7657f4bd2e9138a48a524901775a42b83fca5003f9fb5fa09489c8bfd"},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","de5d74ce6b9d6783565aacfdb03bed71d684850abc235cbc3daeadf62dbb2e04","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"ed548abd8ed1247e4c0ecf4df561b9f8a50182aed3349b92bc2886f5e8ab4f27","signature":"f905edb367c8a8961d4edd4a9e35ec85082a5b90cdc8e2b409f29a49b091f9d7"},"5ad6df80e85553e8ba7b1daccafba0593447ca4efec2d94f772ababeda2a1151","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","36a13310183637f7191deef6608b62a6693e57f32e6ba82aa3b48d833ddfd61d","e84ef765013adf144b02eb3836c3b73ee711c7e24e11a9318f0d53e665369347","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"145e759dbb5bf775433dd3a361bbf56fc01c52da63c6f3b2e1352d35a274500b","impliedFormat":99},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","0a7ed9b81f916c5b49a808a2b05148f4ac7650245a74f4f6929d07096cac658a","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","f4445fa91ab811e238588b2504bf42aad2cb668f4d19bbdbf188f80845fdffbc","6f75832ec7088aa895f07c27680381aec7c0948264b527bf21ed215261b2084f","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","673be3afbc9924250ab068335c1d2a7342cd5678dd895a49fcaad52293a7c210","e289cb288a5269c0a0f5b8ed024c543b1951403c12f1910e591041ff5619df5f","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","8d832ede66d66cb4ed03372b015433676b7ffadf092f4cf5cddc73611f9cc365","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","3fd921fa1db0d7d78aff212c45652647bb490100dc7761bdaeeb2fc820775981","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","6472c96a92598b23ee1b2742c5a2bebb1513b4621d9840bc23d9bad242503da1",{"version":"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","signature":"da14a67372982ca6e605fea114900b492b3316618581634e0ce72afbcb09baca"},"3d8fa3257ead6a82f88c8c8e13c322937e0e66a9033e4b71ca6cc697034aed4b","a0a98d707d930bb136924c663d27378c9f47b24f160f028cb1c7b029ed9c55a1","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"6b9b46d59f6518b6c95a71d4d1a6bf81d5aa334d140f6ab0130ee0d36789b94e","signature":"a90a385f0bff771129a4b6375ca7b432e0814be7124014564d8b5023fc398859"},{"version":"fa8f81d5cd3084cdb1520d6c690a62e34cf63b6fe2de0d6acf1dcf0627e154e4","signature":"5579bb8aaa00ce7978577339d7b0d9b5018c824c46ceb014184e383565b99633"},"5d67131bda32c4c872617c0a46cdb0da9561bf154646b317cf27b7b57db722b8","87749e6df6577dc8d89d6ab2a45d656d1c34a4c4fce72be4409cc9c227258aa1","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","e4374e96ae3b631b9e0eb6899715144c50f6eedd577fa4e559cfdedc15cc6f46",{"version":"a3728b8de3ea689453f136a7d55ef92ee32f9ba4a90e1aeac30406bff82f9799","signature":"0550f55e50180787e38fe2fd01df63157f8aad620d120a5c6951534a5854cb9b"},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","0101b48a4ad5568fd311bd620fbcd39fa06a55f5942fd9c899dcdd5d062670f8","c152d8bcd6bf2c3d7a4583f89b1a5bd7b74865da3e96eca7f769d65baf2650f3","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","a8303224c5555d6cd2f25f893f2c1df3be4bdb24b7298f14ad812eacf17fe03d","6aee99274783e3e0dd29ca3bdc38a3b17172d890b0365124c7582e7c16d8d70f","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","c4703e72d3525ee52b44292c11da4fbdcb4624a3bd862ec0103e0a29aa04c823","977a899584d90b83d5903dee1ee8b8480de917d2ea71ae7ee9ba3fc503a2493b","535c3dfb8390e36e181a33628a7555879d8dcf63b912f38f897083ff2d1478bb","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","04d2a5d32da1bc04b3dcf295df3bb62778b8b9a68d47031bafe302cfd02faf32"],"root":[62,269,[274,366],[368,410]],"options":{"composite":false,"declaration":false,"declarationMap":false,"experimentalDecorators":true,"importHelpers":true,"inlineSourceMap":false,"inlineSources":false,"module":7,"noEmitOnError":false,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noPropertyAccessFromIndexSignature":true,"outDir":"../../../..","removeComments":false,"skipLibCheck":true,"strict":true,"target":9,"tsBuildInfoFile":"./.tsbuildinfo"},"referencedMap":[[281,1],[286,2],[282,1],[283,3],[287,1],[292,4],[293,1],[296,5],[297,1],[300,6],[298,1],[299,7],[301,1],[302,8],[303,1],[308,9],[304,1],[305,10],[306,1],[307,11],[309,1],[310,12],[280,1],[329,13],[284,1],[285,14],[311,1],[312,15],[294,1],[295,16],[288,1],[291,17],[289,1],[290,18],[313,1],[314,19],[315,1],[316,20],[317,1],[318,21],[319,1],[320,22],[321,1],[322,23],[323,1],[324,24],[325,1],[326,25],[327,1],[328,26],[279,1],[330,27],[337,1],[407,28],[269,1],[338,1],[392,29],[336,30],[331,1],[332,31],[393,1],[396,32],[397,1],[400,33],[401,1],[404,34],[276,1],[277,35],[398,1],[399,36],[405,1],[406,37],[402,1],[403,38],[278,1],[333,39],[334,1],[335,40],[361,1],[375,41],[363,1],[364,42],[366,1],[372,43],[373,1],[374,44],[362,1],[365,45],[379,1],[380,46],[388,1],[389,47],[381,1],[386,48],[387,1],[390,49],[384,1],[385,50],[376,1],[391,51],[377,1],[378,52],[339,1],[360,53],[356,1],[357,54],[343,1],[344,55],[274,1],[275,56],[340,1],[341,57],[342,1],[345,58],[382,1],[383,59],[349,1],[350,60],[346,1],[347,61],[348,1],[351,62],[368,1],[369,63],[352,1],[355,64],[370,1],[371,65],[358,1],[359,66],[353,1],[354,67],[409,1],[410,68],[394,1],[395,69],[62,1],[408,70],[262,71],[266,72],[261,72],[264,73],[267,74],[258,75],[65,76],[257,77],[255,78],[260,79],[272,80],[367,81],[265,82],[268,83],[270,84],[271,85],[273,86],[254,87],[205,88],[203,88],[253,89],[218,90],[217,90],[118,91],[69,92],[225,91],[226,91],[228,93],[229,91],[230,94],[129,95],[231,91],[202,91],[232,91],[233,96],[234,91],[235,90],[236,97],[237,91],[238,91],[239,91],[240,91],[241,90],[242,91],[243,91],[244,91],[245,91],[246,98],[247,91],[248,91],[249,91],[250,91],[251,91],[68,89],[71,94],[72,94],[73,94],[74,94],[75,94],[76,94],[77,94],[78,91],[80,99],[81,94],[79,94],[82,94],[83,94],[84,94],[85,94],[86,94],[87,94],[88,91],[89,94],[90,94],[91,94],[92,94],[93,94],[94,91],[95,94],[96,94],[97,94],[98,94],[99,94],[100,94],[101,91],[103,100],[102,94],[104,94],[105,94],[106,94],[107,94],[108,98],[109,91],[110,91],[124,101],[112,102],[113,94],[114,94],[115,91],[116,94],[117,94],[119,103],[120,94],[121,94],[122,94],[123,94],[125,94],[126,94],[127,94],[128,94],[130,104],[131,94],[132,94],[133,94],[134,91],[135,94],[136,105],[137,105],[138,105],[139,91],[140,94],[141,94],[142,94],[147,94],[143,94],[144,91],[145,94],[146,91],[148,94],[149,94],[150,94],[151,94],[152,94],[153,94],[154,91],[155,94],[156,94],[157,94],[158,94],[159,94],[160,94],[161,94],[162,94],[163,94],[164,94],[165,94],[166,94],[167,94],[168,94],[169,94],[170,94],[171,106],[172,94],[173,94],[174,94],[175,94],[176,94],[177,94],[178,91],[179,91],[180,91],[181,91],[182,91],[183,94],[184,94],[185,94],[186,94],[204,107],[252,91],[189,108],[188,109],[212,110],[211,111],[207,112],[206,111],[208,113],[197,114],[195,115],[210,116],[209,113],[198,117],[111,118],[67,119],[66,94],[193,120],[194,121],[192,122],[190,94],[199,123],[70,124],[216,90],[214,125],[187,126],[200,127],[61,128]],"semanticDiagnosticsPerFile":[1,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,62,269,274,276,278,279,280,281,282,284,287,288,289,293,294,297,298,301,303,304,306,309,311,313,315,317,319,321,323,325,327,331,334,337,338,339,340,342,343,346,348,349,352,353,356,358,360,361,362,363,366,368,370,373,374,375,376,377,378,379,380,381,382,384,385,386,387,388,391,392,393,394,397,398,401,402,405,407,408,409],"affectedFilesPendingEmit":[281,286,282,283,287,292,293,296,297,300,298,299,301,302,303,308,304,305,306,307,309,310,280,329,284,285,311,312,294,295,288,291,289,290,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,279,330,337,407,269,338,392,336,331,332,393,396,397,400,401,404,276,277,398,399,405,406,402,403,278,333,334,335,361,375,363,364,366,372,373,374,362,365,379,380,388,389,381,386,387,390,384,385,376,391,377,378,339,360,356,357,343,344,274,275,340,341,342,345,382,383,349,350,346,347,348,351,368,369,352,355,370,371,358,359,353,354,409,410,394,395,62,408],"version":"5.9.3"} \ No newline at end of file diff --git a/apps/frontend/.angular/cache/21.2.16/poc-frontend/angular-compiler.db b/apps/frontend/.angular/cache/21.2.16/poc-frontend/angular-compiler.db new file mode 100644 index 0000000..4ab4188 Binary files /dev/null and b/apps/frontend/.angular/cache/21.2.16/poc-frontend/angular-compiler.db differ diff --git a/apps/frontend/.angular/cache/21.2.16/poc-frontend/angular-compiler.db-lock b/apps/frontend/.angular/cache/21.2.16/poc-frontend/angular-compiler.db-lock new file mode 100644 index 0000000..6c198f9 Binary files /dev/null and b/apps/frontend/.angular/cache/21.2.16/poc-frontend/angular-compiler.db-lock differ diff --git a/apps/frontend/README.md b/apps/frontend/README.md index 14520ca..01463f1 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -1,16 +1,15 @@ # Frontend SPA -React/Vite/TypeScript SPA for the document pipeline PoC. +Angular/TypeScript SPA for the document pipeline PoC. ## Structure -- `src/app`: bootstrap, providers and top-level view composition. -- `src/components`: reusable UI primitives and layout components. -- `src/features`: domain modules for assistant, communications, documents, observability and state. -- `src/api/generated`: Orval generated client. Do not edit manually. -- `src/api/pocApi.ts`: stable adapter around the generated client. -- `src/lib`: formatting, errors and status helpers. +- `src/app`: Angular bootstrap, routes, shell, core services, shared components and feature pages. +- `src/app/core`: navigation model, API interceptors, correlation IDs, structured logging, global errors, theme and state store. +- `src/app/features`: assistant, overview and document Co-Pilot pages. +- `src/api/generated`: Orval generated Angular/HttpClient service and model types. Do not edit manually. - `src/styles`: global tokens, base styles and minimal utilities. +- `public`: static assets copied into the Angular production build. ## Commands @@ -18,9 +17,13 @@ From the repository root: ```bash make openapi-generate +make frontend-lint make frontend-typecheck make frontend-test make frontend-build +make frontend-s3-local-deploy ``` -The root package uses npm workspaces. Keep frontend dependencies in `apps/frontend/package.json` and orchestration scripts in the root `package.json`/`Makefile`. +The root package uses npm workspaces. Frontend commands run through the Docker Compose `node` tool container (`node:22-bookworm-slim`), not through the host Node runtime. + +The app calls Laravel with relative `/api/v1` URLs by default. `proxy.conf.json` keeps `ng serve` aligned with the local Traefik/Nginx entrypoint, while production builds are static and can be served by Nginx or by the LocalStack S3/CloudFront simulation. diff --git a/apps/frontend/angular.json b/apps/frontend/angular.json new file mode 100644 index 0000000..c3b4f8a --- /dev/null +++ b/apps/frontend/angular.json @@ -0,0 +1,88 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "poc-frontend": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "poc", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": { + "base": "dist", + "browser": "" + }, + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles/globals.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "600kB", + "maximumError": "1.2MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "8kB", + "maximumError": "16kB" + } + ], + "outputHashing": "all", + "optimization": { + "scripts": true, + "fonts": true, + "styles": { + "minify": true, + "inlineCritical": false + } + } + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, + "configurations": { + "production": { + "buildTarget": "poc-frontend:build:production" + }, + "development": { + "buildTarget": "poc-frontend:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js new file mode 100644 index 0000000..933840d --- /dev/null +++ b/apps/frontend/eslint.config.js @@ -0,0 +1,36 @@ +// @ts-check +const eslint = require("@eslint/js"); +const tseslint = require("typescript-eslint"); +const angular = require("angular-eslint"); + +module.exports = tseslint.config( + { + files: ["**/*.ts"], + ignores: ["src/api/generated/**"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.stylistic, + ...angular.configs.tsRecommended + ], + processor: angular.processInlineTemplates, + rules: { + "@angular-eslint/directive-selector": [ + "error", + { type: "attribute", prefix: "poc", style: "camelCase" } + ], + "@angular-eslint/component-selector": [ + "error", + { type: "element", prefix: "poc", style: "kebab-case" } + ] + } + }, + { + files: ["**/*.html"], + extends: [ + ...angular.configs.templateRecommended, + ...angular.configs.templateAccessibility + ], + rules: {} + } +); diff --git a/apps/frontend/jest.config.js b/apps/frontend/jest.config.js new file mode 100644 index 0000000..cb595cd --- /dev/null +++ b/apps/frontend/jest.config.js @@ -0,0 +1,19 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: "jest-preset-angular", + setupFilesAfterEnv: ["/src/setup-jest.ts"], + testEnvironment: "jest-preset-angular/environments/jest-jsdom-env", + testMatch: ["/src/**/*.spec.ts"], + moduleNameMapper: { + "^src/(.*)$": "/src/$1" + }, + transform: { + "^.+\\.(ts|mjs|js|html)$": [ + "jest-preset-angular", + { + tsconfig: "/tsconfig.spec.json", + stringifyContentPathRegex: "\\.(html|svg)$" + } + ] + } +}; diff --git a/apps/frontend/orval.config.ts b/apps/frontend/orval.config.ts index 998b830..f5ef631 100644 --- a/apps/frontend/orval.config.ts +++ b/apps/frontend/orval.config.ts @@ -5,13 +5,10 @@ export default defineConfig({ input: "../../openapi/v1/alittlebyte-poc-api.yaml", output: { mode: "single", - client: "fetch", + client: "angular", target: "src/api/generated/poc-api.ts", schemas: "src/api/generated/model", - clean: true, - override: { - requestOptions: true - } + clean: true } } }); diff --git a/apps/frontend/package.json b/apps/frontend/package.json index a87aabc..b0b0ce0 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -2,33 +2,42 @@ "name": "poc-frontend", "private": true, "version": "0.1.0", - "type": "module", "scripts": { - "dev": "vite --host 0.0.0.0", - "build": "tsc -b && vite build", - "preview": "vite preview", - "test": "vitest run --config vitest.config.ts", - "typecheck": "tsc -b --pretty false", + "dev": "ng serve --host 0.0.0.0 --port 5173", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "jest", + "lint": "eslint \"src/**/*.{ts,html}\"", + "typecheck": "tsc -p tsconfig.app.json --noEmit", "openapi:generate": "orval --config ./orval.config.ts && node scripts/write-generated-readme.mjs", "openapi:check": "npm run openapi:generate && git diff --exit-code -- src/api/generated" }, "dependencies": { - "@tanstack/react-query": "^5.90.0", - "lucide-react": "^0.468.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-hook-form": "^7.54.0" + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "@lucide/angular": "^1.21.0", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "~0.15.1" }, "devDependencies": { - "@testing-library/jest-dom": "^6.6.0", - "@testing-library/react": "^16.1.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^5.0.0", - "jsdom": "^25.0.0", - "orval": "^8.16.0", - "typescript": "^5.7.0", - "vite": "^6.0.0", - "vitest": "^4.1.8" + "@angular/build": "^21.2.0", + "@angular/cli": "^21.2.0", + "@angular/compiler-cli": "^21.2.0", + "@types/jest": "^30.0.0", + "angular-eslint": "^21.4.0", + "eslint": "^9.20.0", + "glob": "^13.0.6", + "jest": "^30.0.0", + "jest-preset-angular": "^17.0.0", + "jsdom": "^29.1.1", + "orval": "^8.18.0", + "test-exclude": "^8.0.0", + "typescript": "~5.9.2", + "typescript-eslint": "^8.24.0" } } diff --git a/apps/frontend/proxy.conf.json b/apps/frontend/proxy.conf.json new file mode 100644 index 0000000..ad4261a --- /dev/null +++ b/apps/frontend/proxy.conf.json @@ -0,0 +1,17 @@ +{ + "/api": { + "target": "https://localhost:8443", + "secure": false, + "changeOrigin": true + }, + "/health": { + "target": "https://localhost:8443", + "secure": false, + "changeOrigin": true + }, + "/ready": { + "target": "https://localhost:8443", + "secure": false, + "changeOrigin": true + } +} diff --git a/apps/frontend/src/assets/alittlebyte-logo.png b/apps/frontend/public/alittlebyte-logo.png similarity index 100% rename from apps/frontend/src/assets/alittlebyte-logo.png rename to apps/frontend/public/alittlebyte-logo.png diff --git a/apps/frontend/src/api/generated/model/assistantState.ts b/apps/frontend/src/api/generated/model/assistantState.ts index c385518..f6eff6a 100644 --- a/apps/frontend/src/api/generated/model/assistantState.ts +++ b/apps/frontend/src/api/generated/model/assistantState.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { Communication } from './communication'; diff --git a/apps/frontend/src/api/generated/model/communication.ts b/apps/frontend/src/api/generated/model/communication.ts index 95b740b..6d7f233 100644 --- a/apps/frontend/src/api/generated/model/communication.ts +++ b/apps/frontend/src/api/generated/model/communication.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/copilotState.ts b/apps/frontend/src/api/generated/model/copilotState.ts index 476dc8c..a5c4789 100644 --- a/apps/frontend/src/api/generated/model/copilotState.ts +++ b/apps/frontend/src/api/generated/model/copilotState.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { Metric } from './metric'; diff --git a/apps/frontend/src/api/generated/model/deleteDocumentResponse.ts b/apps/frontend/src/api/generated/model/deleteDocumentResponse.ts index 4c01979..a4d9cf3 100644 --- a/apps/frontend/src/api/generated/model/deleteDocumentResponse.ts +++ b/apps/frontend/src/api/generated/model/deleteDocumentResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { PocState } from './pocState'; diff --git a/apps/frontend/src/api/generated/model/errorEnvelope.ts b/apps/frontend/src/api/generated/model/errorEnvelope.ts index 915cf0c..a69f279 100644 --- a/apps/frontend/src/api/generated/model/errorEnvelope.ts +++ b/apps/frontend/src/api/generated/model/errorEnvelope.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { ErrorEnvelopeError } from './errorEnvelopeError'; diff --git a/apps/frontend/src/api/generated/model/errorEnvelopeError.ts b/apps/frontend/src/api/generated/model/errorEnvelopeError.ts index 8044117..7d09f23 100644 --- a/apps/frontend/src/api/generated/model/errorEnvelopeError.ts +++ b/apps/frontend/src/api/generated/model/errorEnvelopeError.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/forbiddenResponse.ts b/apps/frontend/src/api/generated/model/forbiddenResponse.ts index 3a89cab..e7180a5 100644 --- a/apps/frontend/src/api/generated/model/forbiddenResponse.ts +++ b/apps/frontend/src/api/generated/model/forbiddenResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { ErrorEnvelope } from './errorEnvelope'; diff --git a/apps/frontend/src/api/generated/model/generateCommunicationRequest.ts b/apps/frontend/src/api/generated/model/generateCommunicationRequest.ts index 8000a4f..b0e917f 100644 --- a/apps/frontend/src/api/generated/model/generateCommunicationRequest.ts +++ b/apps/frontend/src/api/generated/model/generateCommunicationRequest.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { GenerateCommunicationRequestStyle } from './generateCommunicationRequestStyle'; diff --git a/apps/frontend/src/api/generated/model/generateCommunicationRequestStyle.ts b/apps/frontend/src/api/generated/model/generateCommunicationRequestStyle.ts index ffe703b..e24d95d 100644 --- a/apps/frontend/src/api/generated/model/generateCommunicationRequestStyle.ts +++ b/apps/frontend/src/api/generated/model/generateCommunicationRequestStyle.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/generateCommunicationRequestTone.ts b/apps/frontend/src/api/generated/model/generateCommunicationRequestTone.ts index 50e07f9..a298fb5 100644 --- a/apps/frontend/src/api/generated/model/generateCommunicationRequestTone.ts +++ b/apps/frontend/src/api/generated/model/generateCommunicationRequestTone.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/generateCommunicationResponse.ts b/apps/frontend/src/api/generated/model/generateCommunicationResponse.ts index f406e46..4ce025d 100644 --- a/apps/frontend/src/api/generated/model/generateCommunicationResponse.ts +++ b/apps/frontend/src/api/generated/model/generateCommunicationResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { Communication } from './communication'; diff --git a/apps/frontend/src/api/generated/model/index.ts b/apps/frontend/src/api/generated/model/index.ts index 24e3b1b..67485c8 100644 --- a/apps/frontend/src/api/generated/model/index.ts +++ b/apps/frontend/src/api/generated/model/index.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/metric.ts b/apps/frontend/src/api/generated/model/metric.ts index 8057867..d9d1750 100644 --- a/apps/frontend/src/api/generated/model/metric.ts +++ b/apps/frontend/src/api/generated/model/metric.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/notFoundResponse.ts b/apps/frontend/src/api/generated/model/notFoundResponse.ts index 310cb24..2779c9c 100644 --- a/apps/frontend/src/api/generated/model/notFoundResponse.ts +++ b/apps/frontend/src/api/generated/model/notFoundResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { ErrorEnvelope } from './errorEnvelope'; diff --git a/apps/frontend/src/api/generated/model/pocState.ts b/apps/frontend/src/api/generated/model/pocState.ts index c63a4d2..e080ef7 100644 --- a/apps/frontend/src/api/generated/model/pocState.ts +++ b/apps/frontend/src/api/generated/model/pocState.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { AssistantState } from './assistantState'; diff --git a/apps/frontend/src/api/generated/model/subDocument.ts b/apps/frontend/src/api/generated/model/subDocument.ts index d23de85..725e8b2 100644 --- a/apps/frontend/src/api/generated/model/subDocument.ts +++ b/apps/frontend/src/api/generated/model/subDocument.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { SubDocumentReviewStatus } from './subDocumentReviewStatus'; diff --git a/apps/frontend/src/api/generated/model/subDocumentReviewStatus.ts b/apps/frontend/src/api/generated/model/subDocumentReviewStatus.ts index 9655d18..72ebd26 100644 --- a/apps/frontend/src/api/generated/model/subDocumentReviewStatus.ts +++ b/apps/frontend/src/api/generated/model/subDocumentReviewStatus.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/unauthorizedResponse.ts b/apps/frontend/src/api/generated/model/unauthorizedResponse.ts index 48b5ecf..a5bf793 100644 --- a/apps/frontend/src/api/generated/model/unauthorizedResponse.ts +++ b/apps/frontend/src/api/generated/model/unauthorizedResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { ErrorEnvelope } from './errorEnvelope'; diff --git a/apps/frontend/src/api/generated/model/updateExtractedDataRequest.ts b/apps/frontend/src/api/generated/model/updateExtractedDataRequest.ts index 5895641..205c7de 100644 --- a/apps/frontend/src/api/generated/model/updateExtractedDataRequest.ts +++ b/apps/frontend/src/api/generated/model/updateExtractedDataRequest.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/updateSubDocumentReviewResponse.ts b/apps/frontend/src/api/generated/model/updateSubDocumentReviewResponse.ts index fa776f4..45d69a8 100644 --- a/apps/frontend/src/api/generated/model/updateSubDocumentReviewResponse.ts +++ b/apps/frontend/src/api/generated/model/updateSubDocumentReviewResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { PocState } from './pocState'; diff --git a/apps/frontend/src/api/generated/model/uploadDocumentResponse.ts b/apps/frontend/src/api/generated/model/uploadDocumentResponse.ts index 37186aa..7ba827b 100644 --- a/apps/frontend/src/api/generated/model/uploadDocumentResponse.ts +++ b/apps/frontend/src/api/generated/model/uploadDocumentResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/uploadPocDocumentBody.ts b/apps/frontend/src/api/generated/model/uploadPocDocumentBody.ts index bd3c63b..a031d19 100644 --- a/apps/frontend/src/api/generated/model/uploadPocDocumentBody.ts +++ b/apps/frontend/src/api/generated/model/uploadPocDocumentBody.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ diff --git a/apps/frontend/src/api/generated/model/upstreamUnavailableResponse.ts b/apps/frontend/src/api/generated/model/upstreamUnavailableResponse.ts index a5bf777..2b33063 100644 --- a/apps/frontend/src/api/generated/model/upstreamUnavailableResponse.ts +++ b/apps/frontend/src/api/generated/model/upstreamUnavailableResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { ErrorEnvelope } from './errorEnvelope'; diff --git a/apps/frontend/src/api/generated/model/validationErrorEnvelope.ts b/apps/frontend/src/api/generated/model/validationErrorEnvelope.ts index 9455ea2..645001f 100644 --- a/apps/frontend/src/api/generated/model/validationErrorEnvelope.ts +++ b/apps/frontend/src/api/generated/model/validationErrorEnvelope.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { ErrorEnvelope } from './errorEnvelope'; diff --git a/apps/frontend/src/api/generated/model/validationErrorResponse.ts b/apps/frontend/src/api/generated/model/validationErrorResponse.ts index 71a4454..f0d49b1 100644 --- a/apps/frontend/src/api/generated/model/validationErrorResponse.ts +++ b/apps/frontend/src/api/generated/model/validationErrorResponse.ts @@ -1,8 +1,8 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ import type { ValidationErrorEnvelope } from './validationErrorEnvelope'; diff --git a/apps/frontend/src/api/generated/poc-api.ts b/apps/frontend/src/api/generated/poc-api.ts index 4419763..ef9a6ae 100644 --- a/apps/frontend/src/api/generated/poc-api.ts +++ b/apps/frontend/src/api/generated/poc-api.ts @@ -1,452 +1,375 @@ /** - * Generated by orval v8.16.0 🍺 + * Generated by orval v8.18.0 🍺 * Do not edit manually. * Alittlebyte PoC API - * Versioned JSON contract consumed by the React SPA. + * Versioned JSON contract consumed by the Angular SPA. * OpenAPI spec version: 1.0.0 */ +import { + HttpClient, + HttpHeaders, + HttpResponse as AngularHttpResponse +} from '@angular/common/http'; +import type { + HttpContext, + HttpEvent, + HttpParams +} from '@angular/common/http'; + +import { + Injectable, + inject +} from '@angular/core'; + +import { + Observable +} from 'rxjs'; + import type { DeleteDocumentResponse, - ForbiddenResponse, GenerateCommunicationRequest, GenerateCommunicationResponse, - NotFoundResponse, PocState, - UnauthorizedResponse, UpdateExtractedDataRequest, UpdateSubDocumentReviewResponse, UploadDocumentResponse, - UploadPocDocumentBody, - UpstreamUnavailableResponse, - ValidationErrorResponse + UploadPocDocumentBody } from './model'; -export type getPocStateResponse200 = { - data: PocState - status: 200 -} - -export type getPocStateResponse401 = { - data: UnauthorizedResponse - status: 401 -} +interface HttpClientOptions { + readonly headers?: HttpHeaders | Record; + readonly context?: HttpContext; + readonly params?: + | HttpParams + | Record>; + readonly reportProgress?: boolean; + readonly withCredentials?: boolean; + readonly credentials?: RequestCredentials; + readonly keepalive?: boolean; + readonly priority?: RequestPriority; + readonly cache?: RequestCache; + readonly mode?: RequestMode; + readonly redirect?: RequestRedirect; + readonly referrer?: string; + readonly integrity?: string; + readonly referrerPolicy?: ReferrerPolicy; + readonly transferCache?: {includeHeaders?: string[]} | boolean; + readonly timeout?: number; +} + +type HttpClientBodyOptions = HttpClientOptions & { + readonly observe?: 'body'; +}; -export type getPocStateResponse403 = { - data: ForbiddenResponse - status: 403 -} +type HttpClientEventOptions = HttpClientOptions & { + readonly observe: 'events'; +}; -export type getPocStateResponseSuccess = (getPocStateResponse200) & { - headers: Headers; +type HttpClientResponseOptions = HttpClientOptions & { + readonly observe: 'response'; }; -export type getPocStateResponseError = (getPocStateResponse401 | getPocStateResponse403) & { - headers: Headers; + +type HttpClientObserveOptions = HttpClientOptions & { + readonly observe?: 'body' | 'events' | 'response'; }; -export type getPocStateResponse = (getPocStateResponseSuccess | getPocStateResponseError) -export const getGetPocStateUrl = () => { - return `/api/v1/state` -} +@Injectable({ providedIn: 'root' }) +export class AlittlebytePoCAPIService { + private readonly http = inject(HttpClient); /** * @summary Get current PoC state */ -export const getPocState = async ( options?: RequestInit): Promise => { - - const res = await fetch(getGetPocStateUrl(), - { - ...options, - method: 'GET' - - + getPocState( options?: HttpClientBodyOptions): Observable; + getPocState( options?: HttpClientEventOptions): Observable>; + getPocState( options?: HttpClientResponseOptions): Observable>; + getPocState( + options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + if (options?.observe === 'events') { + return this.http.get( + `/api/v1/state`,{ + ...(options as Omit, 'observe'>), + observe: 'events', + } + ); + } + + if (options?.observe === 'response') { + return this.http.get( + `/api/v1/state`,{ + ...(options as Omit, 'observe'>), + observe: 'response', + } + ); + } + + return this.http.get( + `/api/v1/state`,{ + ...(options as Omit, 'observe'>), + observe: 'body', + } + ); } -) - - - const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - - const data: getPocStateResponse['data'] = body ? JSON.parse(body) : {} - return { data, status: res.status, headers: res.headers } as getPocStateResponse -} - - - -export type generatePocCommunicationResponse201 = { - data: GenerateCommunicationResponse - status: 201 -} - -export type generatePocCommunicationResponse422 = { - data: ValidationErrorResponse - status: 422 -} - -export type generatePocCommunicationResponse502 = { - data: UpstreamUnavailableResponse - status: 502 -} - -export type generatePocCommunicationResponseSuccess = (generatePocCommunicationResponse201) & { - headers: Headers; -}; -export type generatePocCommunicationResponseError = (generatePocCommunicationResponse422 | generatePocCommunicationResponse502) & { - headers: Headers; -}; - -export type generatePocCommunicationResponse = (generatePocCommunicationResponseSuccess | generatePocCommunicationResponseError) - -export const getGeneratePocCommunicationUrl = () => { - - - - - return `/api/v1/communications` -} /** * @summary Generate a communication draft */ -export const generatePocCommunication = async (generateCommunicationRequest: GenerateCommunicationRequest, options?: RequestInit): Promise => { - - const res = await fetch(getGeneratePocCommunicationUrl(), - { - ...options, - method: 'POST', - headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify(generateCommunicationRequest) + generatePocCommunication(generateCommunicationRequest: GenerateCommunicationRequest, options?: HttpClientBodyOptions): Observable; + generatePocCommunication(generateCommunicationRequest: GenerateCommunicationRequest, options?: HttpClientEventOptions): Observable>; + generatePocCommunication(generateCommunicationRequest: GenerateCommunicationRequest, options?: HttpClientResponseOptions): Observable>; + generatePocCommunication( + generateCommunicationRequest: GenerateCommunicationRequest, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + if (options?.observe === 'events') { + return this.http.post( + `/api/v1/communications`, + generateCommunicationRequest,{ + ...(options as Omit, 'observe'>), + observe: 'events', + } + ); + } + + if (options?.observe === 'response') { + return this.http.post( + `/api/v1/communications`, + generateCommunicationRequest,{ + ...(options as Omit, 'observe'>), + observe: 'response', + } + ); + } + + return this.http.post( + `/api/v1/communications`, + generateCommunicationRequest,{ + ...(options as Omit, 'observe'>), + observe: 'body', + } + ); } -) - - - const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - - const data: generatePocCommunicationResponse['data'] = body ? JSON.parse(body) : {} - return { data, status: res.status, headers: res.headers } as generatePocCommunicationResponse -} - - - -export type uploadPocDocumentResponse202 = { - data: UploadDocumentResponse - status: 202 -} - -export type uploadPocDocumentResponse422 = { - data: ValidationErrorResponse - status: 422 -} - -export type uploadPocDocumentResponseSuccess = (uploadPocDocumentResponse202) & { - headers: Headers; -}; -export type uploadPocDocumentResponseError = (uploadPocDocumentResponse422) & { - headers: Headers; -}; - -export type uploadPocDocumentResponse = (uploadPocDocumentResponseSuccess | uploadPocDocumentResponseError) - -export const getUploadPocDocumentUrl = () => { - - - - - return `/api/v1/documents/ocr` -} /** * @summary Upload a document for OCR and AI extraction */ -export const uploadPocDocument = async (uploadPocDocumentBody: UploadPocDocumentBody, options?: RequestInit): Promise => { - const formData = new FormData(); + uploadPocDocument(uploadPocDocumentBody: UploadPocDocumentBody, options?: HttpClientBodyOptions): Observable; + uploadPocDocument(uploadPocDocumentBody: UploadPocDocumentBody, options?: HttpClientEventOptions): Observable>; + uploadPocDocument(uploadPocDocumentBody: UploadPocDocumentBody, options?: HttpClientResponseOptions): Observable>; + uploadPocDocument( + uploadPocDocumentBody: UploadPocDocumentBody, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> {const formData = new FormData(); formData.append(`document`, uploadPocDocumentBody.document); - const res = await fetch(getUploadPocDocumentUrl(), - { - ...options, - method: 'POST' - , - body: formData + if (options?.observe === 'events') { + return this.http.post( + `/api/v1/documents/ocr`, + formData,{ + ...(options as Omit, 'observe'>), + observe: 'events', + } + ); + } + + if (options?.observe === 'response') { + return this.http.post( + `/api/v1/documents/ocr`, + formData,{ + ...(options as Omit, 'observe'>), + observe: 'response', + } + ); + } + + return this.http.post( + `/api/v1/documents/ocr`, + formData,{ + ...(options as Omit, 'observe'>), + observe: 'body', + } + ); } -) - - - const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - - const data: uploadPocDocumentResponse['data'] = body ? JSON.parse(body) : {} - return { data, status: res.status, headers: res.headers } as uploadPocDocumentResponse -} - - - -export type streamPocDocumentProcessingResponse200 = { - data: string - status: 200 -} - -export type streamPocDocumentProcessingResponse404 = { - data: NotFoundResponse - status: 404 -} - -export type streamPocDocumentProcessingResponseSuccess = (streamPocDocumentProcessingResponse200) & { - headers: Headers; -}; -export type streamPocDocumentProcessingResponseError = (streamPocDocumentProcessingResponse404) & { - headers: Headers; -}; - -export type streamPocDocumentProcessingResponse = (streamPocDocumentProcessingResponseSuccess | streamPocDocumentProcessingResponseError) - -export const getStreamPocDocumentProcessingUrl = (originalDocument: number,) => { - - - - - return `/api/v1/documents/${originalDocument}/stream` -} /** * @summary Stream document processing events */ -export const streamPocDocumentProcessing = async (originalDocument: number, options?: RequestInit): Promise => { - - const res = await fetch(getStreamPocDocumentProcessingUrl(originalDocument), - { - ...options, - method: 'GET' - - + streamPocDocumentProcessing(originalDocument: number, options?: HttpClientBodyOptions): Observable; + streamPocDocumentProcessing(originalDocument: number, options?: HttpClientEventOptions): Observable>; + streamPocDocumentProcessing(originalDocument: number, options?: HttpClientResponseOptions): Observable>; + streamPocDocumentProcessing( + originalDocument: number, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + if (options?.observe === 'events') { + return this.http.get( + `/api/v1/documents/${originalDocument}/stream`,{ + responseType: 'text', + ...(options as Omit, 'observe'>), + observe: 'events',} + ) as Observable>; + } + + if (options?.observe === 'response') { + return this.http.get( + `/api/v1/documents/${originalDocument}/stream`,{ + responseType: 'text', + ...(options as Omit, 'observe'>), + observe: 'response',} + ) as Observable>; + } + + return this.http.get( + `/api/v1/documents/${originalDocument}/stream`,{ + responseType: 'text', + ...(options as Omit, 'observe'>), + observe: 'body',} + ) as Observable; } -) - - const contentType = (res.headers.get('content-type') ?? '').toLowerCase(); - const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - - const data: streamPocDocumentProcessingResponse['data'] = body ? (contentType.includes('json') ? JSON.parse(body) : body) : {} - return { data, status: res.status, headers: res.headers } as streamPocDocumentProcessingResponse -} - - - -export type deletePocSubDocumentResponse200 = { - data: DeleteDocumentResponse - status: 200 -} - -export type deletePocSubDocumentResponse404 = { - data: NotFoundResponse - status: 404 -} - -export type deletePocSubDocumentResponseSuccess = (deletePocSubDocumentResponse200) & { - headers: Headers; -}; -export type deletePocSubDocumentResponseError = (deletePocSubDocumentResponse404) & { - headers: Headers; -}; - -export type deletePocSubDocumentResponse = (deletePocSubDocumentResponseSuccess | deletePocSubDocumentResponseError) - -export const getDeletePocSubDocumentUrl = (subDocument: number,) => { - - - - - return `/api/v1/documents/${subDocument}` -} /** * @summary Delete a processed sub-document */ -export const deletePocSubDocument = async (subDocument: number, options?: RequestInit): Promise => { - - const res = await fetch(getDeletePocSubDocumentUrl(subDocument), - { - ...options, - method: 'DELETE' - - + deletePocSubDocument(subDocument: number, options?: HttpClientBodyOptions): Observable; + deletePocSubDocument(subDocument: number, options?: HttpClientEventOptions): Observable>; + deletePocSubDocument(subDocument: number, options?: HttpClientResponseOptions): Observable>; + deletePocSubDocument( + subDocument: number, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + if (options?.observe === 'events') { + return this.http.delete( + `/api/v1/documents/${subDocument}`,{ + ...(options as Omit, 'observe'>), + observe: 'events', + } + ); + } + + if (options?.observe === 'response') { + return this.http.delete( + `/api/v1/documents/${subDocument}`,{ + ...(options as Omit, 'observe'>), + observe: 'response', + } + ); + } + + return this.http.delete( + `/api/v1/documents/${subDocument}`,{ + ...(options as Omit, 'observe'>), + observe: 'body', + } + ); } -) - - - const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - - const data: deletePocSubDocumentResponse['data'] = body ? JSON.parse(body) : {} - return { data, status: res.status, headers: res.headers } as deletePocSubDocumentResponse -} - - - -export type updatePocSubDocumentExtractedDataResponse200 = { - data: UpdateSubDocumentReviewResponse - status: 200 -} - -export type updatePocSubDocumentExtractedDataResponse403 = { - data: ForbiddenResponse - status: 403 -} - -export type updatePocSubDocumentExtractedDataResponse404 = { - data: NotFoundResponse - status: 404 -} - -export type updatePocSubDocumentExtractedDataResponse422 = { - data: ValidationErrorResponse - status: 422 -} - -export type updatePocSubDocumentExtractedDataResponseSuccess = (updatePocSubDocumentExtractedDataResponse200) & { - headers: Headers; -}; -export type updatePocSubDocumentExtractedDataResponseError = (updatePocSubDocumentExtractedDataResponse403 | updatePocSubDocumentExtractedDataResponse404 | updatePocSubDocumentExtractedDataResponse422) & { - headers: Headers; -}; - -export type updatePocSubDocumentExtractedDataResponse = (updatePocSubDocumentExtractedDataResponseSuccess | updatePocSubDocumentExtractedDataResponseError) - -export const getUpdatePocSubDocumentExtractedDataUrl = (subDocument: number,) => { - - - - - return `/api/v1/documents/${subDocument}/extracted-data` -} /** * @summary Correct extracted data for a sub-document */ -export const updatePocSubDocumentExtractedData = async (subDocument: number, - updateExtractedDataRequest: UpdateExtractedDataRequest, options?: RequestInit): Promise => { - - const res = await fetch(getUpdatePocSubDocumentExtractedDataUrl(subDocument), - { - ...options, - method: 'PUT', - headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify(updateExtractedDataRequest) + updatePocSubDocumentExtractedData(subDocument: number, + updateExtractedDataRequest: UpdateExtractedDataRequest, options?: HttpClientBodyOptions): Observable; + updatePocSubDocumentExtractedData(subDocument: number, + updateExtractedDataRequest: UpdateExtractedDataRequest, options?: HttpClientEventOptions): Observable>; + updatePocSubDocumentExtractedData(subDocument: number, + updateExtractedDataRequest: UpdateExtractedDataRequest, options?: HttpClientResponseOptions): Observable>; + updatePocSubDocumentExtractedData( + subDocument: number, + updateExtractedDataRequest: UpdateExtractedDataRequest, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + if (options?.observe === 'events') { + return this.http.put( + `/api/v1/documents/${subDocument}/extracted-data`, + updateExtractedDataRequest,{ + ...(options as Omit, 'observe'>), + observe: 'events', + } + ); + } + + if (options?.observe === 'response') { + return this.http.put( + `/api/v1/documents/${subDocument}/extracted-data`, + updateExtractedDataRequest,{ + ...(options as Omit, 'observe'>), + observe: 'response', + } + ); + } + + return this.http.put( + `/api/v1/documents/${subDocument}/extracted-data`, + updateExtractedDataRequest,{ + ...(options as Omit, 'observe'>), + observe: 'body', + } + ); } -) - - - const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - - const data: updatePocSubDocumentExtractedDataResponse['data'] = body ? JSON.parse(body) : {} - return { data, status: res.status, headers: res.headers } as updatePocSubDocumentExtractedDataResponse -} - - - -export type reviewPocSubDocumentResponse200 = { - data: UpdateSubDocumentReviewResponse - status: 200 -} - -export type reviewPocSubDocumentResponse403 = { - data: ForbiddenResponse - status: 403 -} - -export type reviewPocSubDocumentResponse404 = { - data: NotFoundResponse - status: 404 -} - -export type reviewPocSubDocumentResponse422 = { - data: ValidationErrorResponse - status: 422 -} - -export type reviewPocSubDocumentResponseSuccess = (reviewPocSubDocumentResponse200) & { - headers: Headers; -}; -export type reviewPocSubDocumentResponseError = (reviewPocSubDocumentResponse403 | reviewPocSubDocumentResponse404 | reviewPocSubDocumentResponse422) & { - headers: Headers; -}; - -export type reviewPocSubDocumentResponse = (reviewPocSubDocumentResponseSuccess | reviewPocSubDocumentResponseError) - -export const getReviewPocSubDocumentUrl = (subDocument: number,) => { - - - - - return `/api/v1/documents/${subDocument}/review` -} /** * @summary Mark a sub-document as manually reviewed */ -export const reviewPocSubDocument = async (subDocument: number, options?: RequestInit): Promise => { - - const res = await fetch(getReviewPocSubDocumentUrl(subDocument), - { - ...options, - method: 'POST' - - + reviewPocSubDocument(subDocument: number, options?: HttpClientBodyOptions): Observable; + reviewPocSubDocument(subDocument: number, options?: HttpClientEventOptions): Observable>; + reviewPocSubDocument(subDocument: number, options?: HttpClientResponseOptions): Observable>; + reviewPocSubDocument( + subDocument: number, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + if (options?.observe === 'events') { + return this.http.post( + `/api/v1/documents/${subDocument}/review`, + undefined,{ + ...(options as Omit, 'observe'>), + observe: 'events', + } + ); + } + + if (options?.observe === 'response') { + return this.http.post( + `/api/v1/documents/${subDocument}/review`, + undefined,{ + ...(options as Omit, 'observe'>), + observe: 'response', + } + ); + } + + return this.http.post( + `/api/v1/documents/${subDocument}/review`, + undefined,{ + ...(options as Omit, 'observe'>), + observe: 'body', + } + ); } -) - - - const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - - const data: reviewPocSubDocumentResponse['data'] = body ? JSON.parse(body) : {} - return { data, status: res.status, headers: res.headers } as reviewPocSubDocumentResponse -} - - - -export type previewPocSubDocumentResponse200 = { - data: Blob - status: 200 -} - -export type previewPocSubDocumentResponse404 = { - data: NotFoundResponse - status: 404 -} - -export type previewPocSubDocumentResponseSuccess = (previewPocSubDocumentResponse200) & { - headers: Headers; -}; -export type previewPocSubDocumentResponseError = (previewPocSubDocumentResponse404) & { - headers: Headers; -}; - -export type previewPocSubDocumentResponse = (previewPocSubDocumentResponseSuccess | previewPocSubDocumentResponseError) - -export const getPreviewPocSubDocumentUrl = (subDocument: number,) => { - - - - - return `/api/v1/documents/${subDocument}/preview` -} /** * @summary Preview a processed sub-document PDF */ -export const previewPocSubDocument = async (subDocument: number, options?: RequestInit): Promise => { - - const res = await fetch(getPreviewPocSubDocumentUrl(subDocument), - { - ...options, - method: 'GET' - - + previewPocSubDocument(subDocument: number, options?: HttpClientBodyOptions): Observable; + previewPocSubDocument(subDocument: number, options?: HttpClientEventOptions): Observable>; + previewPocSubDocument(subDocument: number, options?: HttpClientResponseOptions): Observable>; + previewPocSubDocument( + subDocument: number, options?: HttpClientObserveOptions): Observable | AngularHttpResponse> { + if (options?.observe === 'events') { + return this.http.get( + `/api/v1/documents/${subDocument}/preview`,{ + responseType: 'blob', + ...(options as Omit, 'observe'>), + observe: 'events',} + ) as Observable>; + } + + if (options?.observe === 'response') { + return this.http.get( + `/api/v1/documents/${subDocument}/preview`,{ + responseType: 'blob', + ...(options as Omit, 'observe'>), + observe: 'response',} + ) as Observable>; + } + + return this.http.get( + `/api/v1/documents/${subDocument}/preview`,{ + responseType: 'blob', + ...(options as Omit, 'observe'>), + observe: 'body',} + ) as Observable; } -) - - const body = [204, 205, 304].includes(res.status) ? null : await res.blob(); - const data: previewPocSubDocumentResponse['data'] = body as previewPocSubDocumentResponse['data'] - return { data, status: res.status, headers: res.headers } as previewPocSubDocumentResponse -} +}; diff --git a/apps/frontend/src/api/pocApi.ts b/apps/frontend/src/api/pocApi.ts deleted file mode 100644 index f6099e6..0000000 --- a/apps/frontend/src/api/pocApi.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - deletePocSubDocument as deletePocSubDocumentGenerated, - generatePocCommunication as generatePocCommunicationGenerated, - getPocState as getPocStateGenerated, - reviewPocSubDocument as reviewPocSubDocumentGenerated, - updatePocSubDocumentExtractedData as updatePocSubDocumentExtractedDataGenerated, - uploadPocDocument as uploadPocDocumentGenerated -} from "./generated/poc-api"; -import type { - DeleteDocumentResponse, - GenerateCommunicationRequest, - GenerateCommunicationResponse, - PocState, - UpdateExtractedDataRequest, - UpdateSubDocumentReviewResponse, - UploadDocumentResponse, - UploadPocDocumentBody -} from "./generated/model"; -import { assertApiSuccess } from "../lib/errors"; - -export async function getPocState(): Promise { - return assertApiSuccess(await getPocStateGenerated()); -} - -export async function generatePocCommunication( - payload: GenerateCommunicationRequest -): Promise { - return assertApiSuccess(await generatePocCommunicationGenerated(payload)); -} - -export async function uploadPocDocument(payload: UploadPocDocumentBody): Promise { - return assertApiSuccess(await uploadPocDocumentGenerated(payload)); -} - -export async function deletePocSubDocument(subDocument: number): Promise { - return assertApiSuccess(await deletePocSubDocumentGenerated(subDocument)); -} - -export async function updatePocSubDocumentExtractedData( - subDocument: number, - payload: UpdateExtractedDataRequest -): Promise { - return assertApiSuccess( - await updatePocSubDocumentExtractedDataGenerated(subDocument, payload) - ); -} - -export async function reviewPocSubDocument(subDocument: number): Promise { - return assertApiSuccess(await reviewPocSubDocumentGenerated(subDocument)); -} diff --git a/apps/frontend/src/app/App.test.tsx b/apps/frontend/src/app/App.test.tsx deleted file mode 100644 index a1c6a87..0000000 --- a/apps/frontend/src/app/App.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import { renderWithQueryClient } from "../test/render"; -import { App } from "./App"; - -vi.mock("../api/pocApi", () => ({ - getPocState: vi.fn(async () => ({ - assistant: { - metrics: [ - { value: 1, label: "Contenuti generati" }, - { value: 1, label: "Bozze generate" } - ], - history: [] - }, - copilot: { - metrics: [ - { value: 0, label: "Documenti analizzati" }, - { value: 0, label: "Sotto-documenti rilevati" } - ], - documents: [] - } - })), - generatePocCommunication: vi.fn(), - uploadPocDocument: vi.fn(), - deletePocSubDocument: vi.fn() -})); - -describe("App", () => { - it("renders the operational overview shell", async () => { - renderWithQueryClient(); - - expect(await screen.findByRole("heading", { name: "Overview operativa" })).toBeInTheDocument(); - expect(await screen.findByText("Contenuti generati")).toBeInTheDocument(); - }); -}); diff --git a/apps/frontend/src/app/App.tsx b/apps/frontend/src/app/App.tsx deleted file mode 100644 index e5bf5e9..0000000 --- a/apps/frontend/src/app/App.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Moon, Sun } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; -import alittlebyteLogo from "../assets/alittlebyte-logo.png"; -import { ErrorState } from "../components/feedback/ErrorState"; -import { AppShell } from "../components/layout/AppShell"; -import { PageHeader } from "../components/layout/PageHeader"; -import { Section } from "../components/layout/Section"; -import { SidebarNav } from "../components/layout/SidebarNav"; -import { CommunicationGeneratorPanel } from "../features/assistant/components/CommunicationGeneratorPanel"; -import { GeneratedCommunicationPreview } from "../features/assistant/components/GeneratedCommunicationPreview"; -import { useGenerateCommunication } from "../features/assistant/hooks/useGenerateCommunication"; -import { CommunicationApprovalPanel } from "../features/communications/components/CommunicationApprovalPanel"; -import { DocumentList } from "../features/documents/components/DocumentList"; -import { DocumentUploadPanel } from "../features/documents/components/DocumentUploadPanel"; -import { SubDocumentList } from "../features/documents/components/SubDocumentList"; -import { useDeleteDocument } from "../features/documents/hooks/useDeleteDocument"; -import { useDocumentUpload } from "../features/documents/hooks/useDocumentUpload"; -import { useDocuments } from "../features/documents/hooks/useDocuments"; -import { useReviewSubDocument } from "../features/documents/hooks/useReviewSubDocument"; -import { MetricsPanel } from "../features/observability/components/MetricsPanel"; -import { SystemStatePanel } from "../features/state/components/SystemStatePanel"; -import { usePocState } from "../features/state/hooks/usePocState"; -import { getErrorMessage } from "../lib/errors"; -import { useScrollSpy } from "../hooks/useScrollSpy"; -import { useTheme } from "../hooks/useTheme"; -import { pocNavGroups, pocViewTitles, type PocView } from "./routes"; -import styles from "./App.module.css"; - -export function App() { - const [activeView, setActiveView] = useState("overview"); - const [selectedDraftId, setSelectedDraftId] = useState(null); - const { theme, toggleTheme } = useTheme(); - const stateQuery = usePocState(); - const state = stateQuery.data; - const documents = state?.copilot.documents ?? []; - const history = state?.assistant.history ?? []; - const { selectDocument, selectedDocument, selectedDocumentId } = useDocuments(documents); - const assistant = useGenerateCommunication(() => { - setSelectedDraftId(null); - setActiveView("assistant"); - }); - - const previewDraft = useMemo(() => { - if (selectedDraftId == null) { - return assistant.draft; - } - - const record = history.find((communication) => communication.id === selectedDraftId); - - return record ? { title: record.title, body: record.body, status: record.status } : assistant.draft; - }, [assistant.draft, history, selectedDraftId]); - const upload = useDocumentUpload({ onDocumentReceived: selectDocument }); - const deleteDocument = useDeleteDocument(() => selectDocument(null)); - const reviewDocument = useReviewSubDocument(selectDocument); - - const activeChildIds = useMemo( - () => - pocNavGroups - .flatMap((group) => group.items) - .find((item) => item.id === activeView) - ?.children?.map((child) => child.targetId) ?? [], - [activeView], - ); - const activeChildId = useScrollSpy(activeChildIds); - - const navigate = useCallback((view: PocView, targetId?: string) => { - setActiveView(view); - - window.requestAnimationFrame(() => { - const target = targetId ? document.getElementById(targetId) : document.getElementById("poc-topbar"); - target?.scrollIntoView({ behavior: "smooth", block: "start" }); - }); - }, []); - - return ( - - } - header={ - - - ); -} diff --git a/apps/frontend/src/app/AppProviders.tsx b/apps/frontend/src/app/AppProviders.tsx deleted file mode 100644 index c62ce94..0000000 --- a/apps/frontend/src/app/AppProviders.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - retry: 1 - } - } -}); - -export function AppProviders({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/apps/frontend/src/app/app.config.ts b/apps/frontend/src/app/app.config.ts new file mode 100644 index 0000000..9bc0748 --- /dev/null +++ b/apps/frontend/src/app/app.config.ts @@ -0,0 +1,20 @@ +import { type ApplicationConfig, ErrorHandler, provideZoneChangeDetection } from "@angular/core"; +import { provideRouter, withInMemoryScrolling } from "@angular/router"; +import { provideHttpClient, withInterceptors } from "@angular/common/http"; +import { routes } from "./app.routes"; +import { apiBaseUrlInterceptor } from "./core/interceptors/api-base-url.interceptor"; +import { correlationInterceptor } from "./core/interceptors/correlation.interceptor"; +import { errorLoggingInterceptor } from "./core/interceptors/error-logging.interceptor"; +import { GlobalErrorHandler } from "./core/observability/global-error-handler"; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes, withInMemoryScrolling({ anchorScrolling: "disabled", scrollPositionRestoration: "disabled" })), + provideHttpClient( + // Ordine: base URL -> correlazione -> logging, cosi' il log vede l'URL finale. + withInterceptors([apiBaseUrlInterceptor, correlationInterceptor, errorLoggingInterceptor]) + ), + { provide: ErrorHandler, useClass: GlobalErrorHandler } + ] +}; diff --git a/apps/frontend/src/app/App.module.css b/apps/frontend/src/app/app.css similarity index 82% rename from apps/frontend/src/app/App.module.css rename to apps/frontend/src/app/app.css index 05468bc..ae99195 100644 --- a/apps/frontend/src/app/App.module.css +++ b/apps/frontend/src/app/app.css @@ -33,7 +33,7 @@ transition: transform 160ms ease; } -[data-poc-theme="dark"] .themeToggle::before { +:host-context([data-poc-theme="dark"]) .themeToggle::before { transform: translateX(32px); } @@ -48,10 +48,10 @@ color: var(--poc-muted); } -[data-poc-theme="dark"] .themeToggle svg { +:host-context([data-poc-theme="dark"]) .themeToggle svg { color: var(--poc-muted); } -[data-poc-theme="dark"] .themeToggle svg + svg { +:host-context([data-poc-theme="dark"]) .themeToggle svg + svg { color: #ffffff; } diff --git a/apps/frontend/src/app/app.routes.ts b/apps/frontend/src/app/app.routes.ts new file mode 100644 index 0000000..a83903c --- /dev/null +++ b/apps/frontend/src/app/app.routes.ts @@ -0,0 +1,31 @@ +import type { Routes } from "@angular/router"; +import type { PocView } from "./core/navigation/app-views"; + +/** + * Le tre viste applicative (Overview, Assistant, Co-Pilot) diventano rotte di + * primo livello con lazy loading. Lo store di stato e' a singleton di root, + * quindi il passaggio tra viste resta istantaneo come nella SPA originale: + * il routing aggiunge solo l'indirizzabilita' dell'URL, senza ricaricare i dati. + */ +export const routes: Routes = [ + { path: "", pathMatch: "full", redirectTo: "overview" }, + { + path: "overview", + title: "NEXUM - Overview", + data: { view: "overview" satisfies PocView }, + loadComponent: () => import("./features/overview/overview-page").then((m) => m.OverviewPage) + }, + { + path: "assistant", + title: "NEXUM - AI Assistant", + data: { view: "assistant" satisfies PocView }, + loadComponent: () => import("./features/assistant/assistant-page").then((m) => m.AssistantPage) + }, + { + path: "copilot", + title: "NEXUM - Copilot CdL", + data: { view: "copilot" satisfies PocView }, + loadComponent: () => import("./features/copilot/copilot-page").then((m) => m.CopilotPage) + }, + { path: "**", redirectTo: "overview" } +]; diff --git a/apps/frontend/src/app/app.ts b/apps/frontend/src/app/app.ts new file mode 100644 index 0000000..8aeb40a --- /dev/null +++ b/apps/frontend/src/app/app.ts @@ -0,0 +1,207 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal +} from "@angular/core"; +import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { auditTime, filter, fromEvent } from "rxjs"; +import { LucideArrowUp, LucideMoon, LucideSun } from "@lucide/angular"; +import { ButtonComponent } from "./shared/components/button/button"; +import { + isPocView, + pocNavGroups, + pocViewTitles, + type PocView +} from "./core/navigation/app-views"; +import { PocStateStore } from "./core/state/poc-state.store"; +import { ThemeService } from "./core/theme/theme.service"; + +@Component({ + selector: "poc-root", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ButtonComponent, LucideArrowUp, LucideMoon, LucideSun, RouterOutlet], + template: ` +
+ + +
+
+
+

NEXUM

+

{{ title() }}

+
+ +
+ +
+ +
+ + +
+
+ `, + styleUrls: [ + "./layout/app-shell/app-shell.css", + "./layout/sidebar-nav/sidebar-nav.css", + "./layout/page-header/page-header.css", + "./app.css" + ] +}) +export class AppComponent { + protected readonly navGroups = pocNavGroups; + protected readonly activeView = signal("overview"); + protected readonly activeChildId = signal(null); + /** Il pulsante "torna su" compare solo dopo aver scrollato la pagina. */ + protected readonly showBackToTop = signal(false); + protected readonly title = computed(() => pocViewTitles[this.activeView()]); + protected readonly theme = inject(ThemeService); + private readonly router = inject(Router); + private readonly store = inject(PocStateStore); + private readonly activeChildIds = computed( + () => + this.navGroups + .flatMap((group) => group.items) + .find((item) => item.id === this.activeView()) + ?.children?.map((child) => child.targetId) ?? [] + ); + + constructor() { + this.store.loadOnce(); + this.syncActiveView(this.router.url); + + this.router.events + .pipe( + filter((event): event is NavigationEnd => event instanceof NavigationEnd), + takeUntilDestroyed() + ) + .subscribe((event) => this.syncActiveView(event.urlAfterRedirects)); + + // Il pulsante "torna su" e' utile solo quando c'e' contenuto sopra la + // viewport: lo si mostra oltre una soglia di scroll (listener passivo, + // throttling per non gravare sul rendering). + fromEvent(window, "scroll", { passive: true }) + .pipe(auditTime(120), takeUntilDestroyed()) + .subscribe(() => this.updateBackToTopVisibility()); + this.updateBackToTopVisibility(); + + effect((onCleanup) => { + const ids = this.activeChildIds(); + + if (typeof IntersectionObserver !== "function" || ids.length === 0) { + this.activeChildId.set(ids[0] ?? null); + return; + } + + let observer: IntersectionObserver | null = null; + const frameId = window.requestAnimationFrame(() => { + observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0]; + + if (visible?.target.id) { + this.activeChildId.set(visible.target.id); + } + }, + { rootMargin: "-18% 0px -65% 0px", threshold: 0.01 } + ); + + for (const id of ids) { + const element = document.getElementById(id); + + if (element) { + observer.observe(element); + } + } + }); + + onCleanup(() => { + window.cancelAnimationFrame(frameId); + observer?.disconnect(); + }); + }); + } + + protected navigate(view: PocView, targetId?: string): void { + void this.router.navigate([view]).then(() => this.scrollTo(targetId ?? "poc-topbar")); + } + + protected scrollToTop(): void { + this.scrollTo("poc-topbar"); + } + + private updateBackToTopVisibility(): void { + this.showBackToTop.set(window.scrollY > 320); + } + + private syncActiveView(url: string): void { + const firstSegment = url.split("?")[0]?.split("#")[0]?.split("/").filter(Boolean)[0]; + this.activeView.set(isPocView(firstSegment) ? firstSegment : "overview"); + } + + private scrollTo(elementId: string): void { + window.requestAnimationFrame(() => { + document.getElementById(elementId)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); + } +} diff --git a/apps/frontend/src/app/core/errors/api-error.ts b/apps/frontend/src/app/core/errors/api-error.ts new file mode 100644 index 0000000..f71b6eb --- /dev/null +++ b/apps/frontend/src/app/core/errors/api-error.ts @@ -0,0 +1,64 @@ +import { HttpErrorResponse } from "@angular/common/http"; + +/** + * Traduce un errore qualsiasi in un messaggio leggibile per l'utente. + * + * Il backend risponde con l'envelope `{ error: { message, code, requestId, + * correlationId } }`: si usa il `message` quando presente. Non si espongono mai + * dettagli tecnici (stack, corpo grezzo) nei messaggi mostrati a video, in linea + * con OWASP ASVS (gestione errori senza leak informativi). + */ +export function getApiErrorMessage(error: unknown, fallback = "Operazione non disponibile."): string { + if (error instanceof HttpErrorResponse) { + const message = extractEnvelopeMessage(error.error); + + if (message) { + return message; + } + + if (error.status === 0) { + return "Servizio non raggiungibile. Verifica la connessione e riprova."; + } + + return fallback; + } + + if (error instanceof Error) { + return error.message || fallback; + } + + return extractEnvelopeMessage(error) ?? fallback; +} + +/** Estrae l'eventuale correlation id dall'envelope di errore, per la sola diagnostica. */ +export function extractCorrelationId(error: unknown): string | null { + if (error instanceof HttpErrorResponse) { + const headerValue = error.headers?.get("X-Correlation-ID"); + + if (headerValue) { + return headerValue; + } + + const body = error.error; + + if (typeof body === "object" && body !== null && "error" in body) { + const envelope = (body as { error?: { correlationId?: string | null } }).error; + + return envelope?.correlationId ?? null; + } + } + + return null; +} + +function extractEnvelopeMessage(body: unknown): string | null { + if (typeof body === "object" && body !== null && "error" in body) { + const envelope = (body as { error?: { message?: string } }).error; + + if (envelope && typeof envelope.message === "string" && envelope.message.trim() !== "") { + return envelope.message; + } + } + + return null; +} diff --git a/apps/frontend/src/app/core/interceptors/api-base-url.interceptor.ts b/apps/frontend/src/app/core/interceptors/api-base-url.interceptor.ts new file mode 100644 index 0000000..aef6744 --- /dev/null +++ b/apps/frontend/src/app/core/interceptors/api-base-url.interceptor.ts @@ -0,0 +1,24 @@ +import type { HttpInterceptorFn } from "@angular/common/http"; +import { environment } from "../../../environments/environment"; + +/** + * Premette `environment.apiBaseUrl` alle sole richieste applicative (`/api`, + * `/health`, `/ready`) quando configurato. Di default e' vuoto: SPA e API + * condividono l'origine dietro il reverse proxy, quindi e' un no-op. Resta il + * punto unico per puntare il frontend a un backend su origine diversa. + */ +export const apiBaseUrlInterceptor: HttpInterceptorFn = (req, next) => { + const baseUrl = environment.apiBaseUrl; + + if (!baseUrl || !isRelativeApiUrl(req.url)) { + return next(req); + } + + const normalizedBase = baseUrl.replace(/\/$/, ""); + + return next(req.clone({ url: normalizedBase + req.url })); +}; + +function isRelativeApiUrl(url: string): boolean { + return url.startsWith("/api") || url.startsWith("/health") || url.startsWith("/ready"); +} diff --git a/apps/frontend/src/app/core/interceptors/correlation.interceptor.ts b/apps/frontend/src/app/core/interceptors/correlation.interceptor.ts new file mode 100644 index 0000000..7e39243 --- /dev/null +++ b/apps/frontend/src/app/core/interceptors/correlation.interceptor.ts @@ -0,0 +1,29 @@ +import type { HttpInterceptorFn } from "@angular/common/http"; +import { + CORRELATION_ID_HEADER, + REQUEST_ID_HEADER, + generateRequestId +} from "../observability/correlation"; + +/** + * Propaga gli header di correlazione verso Laravel per ogni richiesta API. + * Il middleware backend li riusa se presenti, cosi' la richiesta originata dalla + * SPA si ritrova negli stessi log/traces lato server. Header tecnici, nessun + * dato sensibile. + */ +export const correlationInterceptor: HttpInterceptorFn = (req, next) => { + if (req.headers.has(REQUEST_ID_HEADER)) { + return next(req); + } + + const requestId = generateRequestId(); + + return next( + req.clone({ + setHeaders: { + [REQUEST_ID_HEADER]: requestId, + [CORRELATION_ID_HEADER]: requestId + } + }) + ); +}; diff --git a/apps/frontend/src/app/core/interceptors/error-logging.interceptor.ts b/apps/frontend/src/app/core/interceptors/error-logging.interceptor.ts new file mode 100644 index 0000000..a7f6346 --- /dev/null +++ b/apps/frontend/src/app/core/interceptors/error-logging.interceptor.ts @@ -0,0 +1,38 @@ +import type { HttpInterceptorFn } from "@angular/common/http"; +import { HttpErrorResponse } from "@angular/common/http"; +import { inject } from "@angular/core"; +import { catchError, throwError } from "rxjs"; +import { LoggerService } from "../observability/logger.service"; +import { CORRELATION_ID_HEADER } from "../observability/correlation"; +import { extractCorrelationId } from "../errors/api-error"; + +/** + * Traccia in modo strutturato i fallimenti HTTP (status, metodo, endpoint, + * correlation id) e poi rilancia l'errore: la gestione utente resta a carico + * delle feature. Non si registra mai il corpo della risposta per evitare leak di + * dati sensibili (OWASP ASVS / Google SRE: segnali utili, niente PII). + */ +export const errorLoggingInterceptor: HttpInterceptorFn = (req, next) => { + const logger = inject(LoggerService); + + return next(req).pipe( + catchError((error: unknown) => { + if (error instanceof HttpErrorResponse) { + logger.error("Richiesta HTTP fallita", { + scope: stripQuery(req.url), + status: error.status, + method: req.method, + correlationId: extractCorrelationId(error) ?? req.headers.get(CORRELATION_ID_HEADER) + }); + } + + return throwError(() => error); + }) + ); +}; + +function stripQuery(url: string): string { + const index = url.indexOf("?"); + + return index === -1 ? url : url.slice(0, index); +} diff --git a/apps/frontend/src/app/routes.tsx b/apps/frontend/src/app/core/navigation/app-views.ts similarity index 65% rename from apps/frontend/src/app/routes.tsx rename to apps/frontend/src/app/core/navigation/app-views.ts index 85a9240..9b73ec8 100644 --- a/apps/frontend/src/app/routes.tsx +++ b/apps/frontend/src/app/core/navigation/app-views.ts @@ -1,8 +1,27 @@ -import type { SidebarNavGroup } from "../components/layout/SidebarNav"; - +/** + * Modello di navigazione della SPA: voci, etichette e anchor della shell + * operativa. Le sezioni figlie sono gli id DOM verso cui scorrere e che lo + * scroll-spy evidenzia nella sidebar. + */ export type PocView = "overview" | "assistant" | "copilot"; -export const pocNavGroups: SidebarNavGroup[] = [ +export interface SidebarNavChild { + label: string; + targetId: string; +} + +export interface SidebarNavItem { + id: PocView; + label: string; + children?: SidebarNavChild[]; +} + +export interface SidebarNavGroup { + title: string; + items: SidebarNavItem[]; +} + +export const pocNavGroups: SidebarNavGroup[] = [ { title: "Panoramica", items: [ @@ -51,3 +70,7 @@ export const pocViewTitles: Record = { assistant: "AI Assistant Generativo", copilot: "AI Co-Pilot per i CdL" }; + +export function isPocView(value: unknown): value is PocView { + return value === "overview" || value === "assistant" || value === "copilot"; +} diff --git a/apps/frontend/src/app/core/observability/correlation.spec.ts b/apps/frontend/src/app/core/observability/correlation.spec.ts new file mode 100644 index 0000000..d8f1453 --- /dev/null +++ b/apps/frontend/src/app/core/observability/correlation.spec.ts @@ -0,0 +1,21 @@ +import { + CORRELATION_ID_HEADER, + REQUEST_ID_HEADER, + generateRequestId +} from "./correlation"; + +describe("frontend correlation", () => { + it("uses backend-compatible header names", () => { + expect(REQUEST_ID_HEADER).toBe("X-Request-ID"); + expect(CORRELATION_ID_HEADER).toBe("X-Correlation-ID"); + }); + + it("generates opaque request identifiers", () => { + const first = generateRequestId(); + const second = generateRequestId(); + + expect(first).toEqual(expect.any(String)); + expect(first.length).toBeGreaterThan(8); + expect(second).not.toBe(first); + }); +}); diff --git a/apps/frontend/src/app/core/observability/correlation.ts b/apps/frontend/src/app/core/observability/correlation.ts new file mode 100644 index 0000000..f7ce427 --- /dev/null +++ b/apps/frontend/src/app/core/observability/correlation.ts @@ -0,0 +1,23 @@ +/** + * Header di correlazione condivisi con il backend Laravel. + * Il middleware `CorrelateRequests` accetta in ingresso `X-Request-ID` e + * `X-Correlation-ID`: generandoli lato browser le richieste originate dalla SPA + * diventano correlabili end-to-end (browser -> proxy -> Laravel -> log/traces). + */ +export const REQUEST_ID_HEADER = "X-Request-ID"; +export const CORRELATION_ID_HEADER = "X-Correlation-ID"; + +/** + * Genera un identificativo opaco per la richiesta. Usa `crypto.randomUUID` + * quando disponibile, con un fallback robusto per ambienti privi dell'API. + */ +export function generateRequestId(): string { + const globalCrypto = globalThis.crypto; + + if (globalCrypto && typeof globalCrypto.randomUUID === "function") { + return globalCrypto.randomUUID(); + } + + // Fallback non crittografico: sufficiente per la sola correlazione dei log. + return "req-" + Date.now().toString(16) + "-" + Math.random().toString(16).slice(2, 10); +} diff --git a/apps/frontend/src/app/core/observability/global-error-handler.ts b/apps/frontend/src/app/core/observability/global-error-handler.ts new file mode 100644 index 0000000..7f5ab6a --- /dev/null +++ b/apps/frontend/src/app/core/observability/global-error-handler.ts @@ -0,0 +1,28 @@ +import { ErrorHandler, Injectable, inject } from "@angular/core"; +import { HttpErrorResponse } from "@angular/common/http"; +import { LoggerService } from "./logger.service"; + +/** + * Gestione centralizzata degli errori non catturati dell'applicazione. + * Gli errori HTTP sono gia' tracciati dall'interceptor dedicato e gestiti dalle + * feature, quindi qui si evita il doppio log: si registra solo cio' che + * sfuggirebbe altrimenti (errori di rendering/logica), senza dati sensibili. + */ +@Injectable() +export class GlobalErrorHandler implements ErrorHandler { + private readonly logger = inject(LoggerService); + + handleError(error: unknown): void { + if (error instanceof HttpErrorResponse) { + // Gia' loggato e mappato a livello di interceptor/feature. + return; + } + + const message = error instanceof Error ? error.message : "Errore applicativo non gestito"; + this.logger.error(message, { scope: "global" }); + + if (error instanceof Error && error.stack) { + console.error(error.stack); + } + } +} diff --git a/apps/frontend/src/app/core/observability/logger.service.ts b/apps/frontend/src/app/core/observability/logger.service.ts new file mode 100644 index 0000000..a78d000 --- /dev/null +++ b/apps/frontend/src/app/core/observability/logger.service.ts @@ -0,0 +1,54 @@ +import { Injectable, isDevMode } from "@angular/core"; + +export type LogLevel = "info" | "warn" | "error"; + +export interface LogContext { + /** Identificativo di correlazione propagato al backend, se disponibile. */ + correlationId?: string | null; + /** Endpoint/area logica coinvolta (mai il payload). */ + scope?: string; + /** Codice di stato HTTP per gli errori di rete. */ + status?: number; + [key: string]: unknown; +} + +/** + * Logger frontend strutturato e volutamente parco. + * + * Principi (Google SRE / OWASP ASVS): segnali diagnostici utili senza rumore e + * senza mai registrare dati sensibili. Il logger riceve solo metadati tecnici + * (endpoint, stato HTTP, correlation id): non si passano mai token, header di + * autenticazione, PII o corpi di richiesta/risposta. + */ +@Injectable({ providedIn: "root" }) +export class LoggerService { + info(message: string, context: LogContext = {}): void { + this.emit("info", message, context); + } + + warn(message: string, context: LogContext = {}): void { + this.emit("warn", message, context); + } + + error(message: string, context: LogContext = {}): void { + this.emit("error", message, context); + } + + private emit(level: LogLevel, message: string, context: LogContext): void { + const entry = { + ts: new Date().toISOString(), + level, + source: "poc-frontend", + message, + ...context + }; + + // In produzione si mantengono solo warn/error per ridurre il rumore. + if (level === "info" && !isDevMode()) { + return; + } + + const sink = level === "error" ? console.error : level === "warn" ? console.warn : console.info; + sink(JSON.stringify(entry)); + } +} diff --git a/apps/frontend/src/app/core/state/poc-state.store.ts b/apps/frontend/src/app/core/state/poc-state.store.ts new file mode 100644 index 0000000..886d5e7 --- /dev/null +++ b/apps/frontend/src/app/core/state/poc-state.store.ts @@ -0,0 +1,86 @@ +import { Injectable, computed, inject, signal } from "@angular/core"; +import { retry } from "rxjs"; +import { AlittlebytePoCAPIService } from "../../../api/generated/poc-api"; +import type { PocState, SubDocument } from "../../../api/generated/model"; +import { getApiErrorMessage } from "../errors/api-error"; + +/** + * Sorgente unica dello stato applicativo (assistant + co-pilot), condivisa fra + * tutte le viste. Essendo un singleton di root, lo stato sopravvive ai cambi di + * rotta e il passaggio fra viste resta istantaneo. Le mutazioni (genera, upload, + * revisione, eliminazione) rimpiazzano lo stato con quello autorevole restituito + * dal backend. + */ +@Injectable({ providedIn: "root" }) +export class PocStateStore { + private readonly api = inject(AlittlebytePoCAPIService); + + private readonly _state = signal(null); + private readonly _loading = signal(false); + private readonly _error = signal(null); + private loadRequested = false; + + readonly state = this._state.asReadonly(); + readonly loading = this._loading.asReadonly(); + readonly error = this._error.asReadonly(); + + readonly documents = computed(() => this._state()?.copilot.documents ?? []); + readonly history = computed(() => this._state()?.assistant.history ?? []); + readonly assistantMetrics = computed(() => this._state()?.assistant.metrics ?? []); + readonly copilotMetrics = computed(() => this._state()?.copilot.metrics ?? []); + + /** Carica lo stato una sola volta (al primo montaggio della shell). */ + loadOnce(): void { + if (this.loadRequested) { + return; + } + + this.loadRequested = true; + this.reload(); + } + + /** Ricarica lo stato applicando una sola retry per errori temporanei. */ + reload(): void { + this._loading.set(true); + this._error.set(null); + + this.api + .getPocState() + .pipe(retry(1)) + .subscribe({ + next: (state) => { + this._state.set(state); + this._loading.set(false); + }, + error: (error: unknown) => { + this._error.set(getApiErrorMessage(error)); + this._loading.set(false); + } + }); + } + + /** Rimpiazza lo stato con quello autorevole restituito da una mutazione. */ + setState(state: PocState): void { + this._state.set(state); + } + + /** + * Inserisce/aggiorna in testa un sotto-documento ricevuto dallo stream SSE, + * preservando il resto dello stato (aggiornamento incrementale dell'upload). + */ + upsertDocument(document: SubDocument): void { + this._state.update((current) => { + if (!current) { + return current; + } + + return { + ...current, + copilot: { + ...current.copilot, + documents: [document, ...current.copilot.documents.filter((item) => item.id !== document.id)] + } + }; + }); + } +} diff --git a/apps/frontend/src/app/core/theme/theme.service.ts b/apps/frontend/src/app/core/theme/theme.service.ts new file mode 100644 index 0000000..282c52b --- /dev/null +++ b/apps/frontend/src/app/core/theme/theme.service.ts @@ -0,0 +1,53 @@ +import { Injectable, effect, signal } from "@angular/core"; + +export type Theme = "light" | "dark"; + +const STORAGE_KEY = "poc-theme"; + +/** + * Tema chiaro/scuro: preferenza persistita in `localStorage`, fallback su + * `prefers-color-scheme` e attributo `data-poc-theme` applicato a `` + * (su cui agiscono i token CSS). + */ +@Injectable({ providedIn: "root" }) +export class ThemeService { + private readonly _theme = signal(getInitialTheme()); + readonly theme = this._theme.asReadonly(); + + constructor() { + effect(() => { + const theme = this._theme(); + document.documentElement.dataset["pocTheme"] = theme; + + try { + window.localStorage.setItem(STORAGE_KEY, theme); + } catch { + // localStorage non disponibile (es. modalita' privata): si ignora. + } + }); + } + + toggle(): void { + this._theme.update((current) => (current === "dark" ? "light" : "dark")); + } +} + +function getInitialTheme(): Theme { + let stored: string | null = null; + + try { + stored = window.localStorage.getItem(STORAGE_KEY); + } catch { + stored = null; + } + + if (stored === "dark" || stored === "light") { + return stored; + } + + if (typeof window.matchMedia !== "function") { + return "light"; + } + + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} diff --git a/apps/frontend/src/app/features/assistant/assistant-page.ts b/apps/frontend/src/app/features/assistant/assistant-page.ts new file mode 100644 index 0000000..0eadd29 --- /dev/null +++ b/apps/frontend/src/app/features/assistant/assistant-page.ts @@ -0,0 +1,122 @@ +import { ChangeDetectionStrategy, Component, computed, inject, signal } from "@angular/core"; +import { finalize } from "rxjs"; +import { AssistantService } from "./data/assistant.service"; +import type { Communication } from "../../../api/generated/model"; +import { PocStateStore } from "../../core/state/poc-state.store"; +import { getApiErrorMessage } from "../../core/errors/api-error"; +import { EmptyStateComponent } from "../../shared/components/empty-state/empty-state"; +import { ErrorStateComponent } from "../../shared/components/error-state/error-state"; +import { SectionComponent } from "../../layout/section/section"; +import { StatusBadgeComponent } from "../../shared/components/status-badge/status-badge"; +import { formatFallback } from "../../shared/util/formatters"; +import { CommunicationGeneratorPanelComponent } from "./components/communication-generator-panel"; +import { GeneratedCommunicationPreviewComponent } from "./components/generated-communication-preview"; +import type { CommunicationDraftForm, GeneratedDraft } from "./assistant.model"; + +@Component({ + selector: "poc-assistant-page", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommunicationGeneratorPanelComponent, + EmptyStateComponent, + ErrorStateComponent, + GeneratedCommunicationPreviewComponent, + SectionComponent, + StatusBadgeComponent + ], + template: ` +
+ @if (store.error(); as error) { + + } + + + + + + @if (history().length) { + @for (communication of history(); track communication.id) { + + } + } @else { + Le bozze generate compariranno qui. + } + +
+ `, + styleUrls: ["./components/communication-status-card.css", "../overview/overview-page.css"] +}) +export class AssistantPage { + protected readonly store = inject(PocStateStore); + protected readonly history = this.store.history; + protected readonly isGenerating = signal(false); + protected readonly status = signal("In attesa di istruzioni."); + protected readonly selectedDraftId = signal(null); + protected readonly latestDraft = signal(null); + protected readonly formatFallback = formatFallback; + protected readonly previewDraft = computed(() => { + const selectedId = this.selectedDraftId(); + + if (selectedId === null) { + return this.latestDraft(); + } + + const record = this.history().find((communication) => communication.id === selectedId); + return record ? this.toDraft(record) : this.latestDraft(); + }); + + private readonly assistant = inject(AssistantService); + + protected generate(payload: CommunicationDraftForm): void { + this.isGenerating.set(true); + this.status.set("Generazione in corso."); + + this.assistant + .generate(payload) + .pipe(finalize(() => this.isGenerating.set(false))) + .subscribe({ + next: (response) => { + this.selectedDraftId.set(null); + this.latestDraft.set(this.toDraft(response.communication)); + this.status.set(response.message); + this.scrollTo("assistant-review"); + }, + error: (error: unknown) => { + this.status.set(getApiErrorMessage(error, "Generazione non disponibile.")); + } + }); + } + + protected selectDraft(communicationId: number): void { + this.selectedDraftId.set(communicationId); + this.scrollTo("assistant-review"); + } + + private toDraft(communication: Communication): GeneratedDraft { + return { + title: communication.title, + body: communication.body, + status: communication.status + }; + } + + private scrollTo(elementId: string): void { + window.requestAnimationFrame(() => { + document.getElementById(elementId)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); + } +} diff --git a/apps/frontend/src/features/assistant/types.ts b/apps/frontend/src/app/features/assistant/assistant.model.ts similarity index 76% rename from apps/frontend/src/features/assistant/types.ts rename to apps/frontend/src/app/features/assistant/assistant.model.ts index 2bb6d75..f4a01b8 100644 --- a/apps/frontend/src/features/assistant/types.ts +++ b/apps/frontend/src/app/features/assistant/assistant.model.ts @@ -1,12 +1,12 @@ -import type { GenerateCommunicationRequest } from "../../api/generated/model"; +import type { GenerateCommunicationRequest } from "../../../api/generated/model"; export type CommunicationDraftForm = GenerateCommunicationRequest; -export type GeneratedDraft = { +export interface GeneratedDraft { body: string; status: string; title: string; -}; +} export const communicationTones = [ "Chiaro e diretto", diff --git a/apps/frontend/src/features/assistant/components/CommunicationGeneratorPanel.module.css b/apps/frontend/src/app/features/assistant/components/communication-generator-panel.css similarity index 93% rename from apps/frontend/src/features/assistant/components/CommunicationGeneratorPanel.module.css rename to apps/frontend/src/app/features/assistant/components/communication-generator-panel.css index d598569..e320233 100644 --- a/apps/frontend/src/features/assistant/components/CommunicationGeneratorPanel.module.css +++ b/apps/frontend/src/app/features/assistant/components/communication-generator-panel.css @@ -30,3 +30,7 @@ grid-template-columns: 1fr; } } + +:host { + display: contents; +} diff --git a/apps/frontend/src/app/features/assistant/components/communication-generator-panel.ts b/apps/frontend/src/app/features/assistant/components/communication-generator-panel.ts new file mode 100644 index 0000000..cbc9db6 --- /dev/null +++ b/apps/frontend/src/app/features/assistant/components/communication-generator-panel.ts @@ -0,0 +1,79 @@ +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { LucideSend } from "@lucide/angular"; +import { ButtonComponent } from "../../../shared/components/button/button"; +import { SelectFieldComponent } from "../../../shared/components/select-field/select-field"; +import { TextAreaFieldComponent } from "../../../shared/components/textarea-field/textarea-field"; +import { SectionComponent } from "../../../layout/section/section"; +import { + communicationStyles, + communicationTones, + type CommunicationDraftForm +} from "../assistant.model"; +import type { + GenerateCommunicationRequestStyle, + GenerateCommunicationRequestTone +} from "../../../../api/generated/model"; + +@Component({ + selector: "poc-communication-generator-panel", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ButtonComponent, + LucideSend, + ReactiveFormsModule, + SectionComponent, + SelectFieldComponent, + TextAreaFieldComponent + ], + template: ` + +

+ Inserisci cosa comunicare e pochi parametri editoriali. Genera la bozza e rivedi titolo e testo nell'anteprima. +

+
+ +
+ + +
+ + +

{{ status() }}

+
+ `, + styleUrl: "./communication-generator-panel.css" +}) +export class CommunicationGeneratorPanelComponent { + readonly isGenerating = input.required(); + readonly status = input.required(); + readonly generate = output(); + + protected readonly tones = communicationTones; + protected readonly styles = communicationStyles; + protected readonly form = new FormGroup({ + prompt: new FormControl( + "Scrivi una comunicazione interna per informare i dipendenti che la nuova area documentale NEXUM e disponibile. Spiega cosa cambia, dove trovare i documenti e perche la consultazione diventa piu semplice.", + { nonNullable: true, validators: [Validators.required, Validators.minLength(12)] } + ), + tone: new FormControl("Chiaro e diretto", { nonNullable: true }), + style: new FormControl("Testo informativo", { nonNullable: true }) + }); + + protected submit(): void { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + + this.generate.emit(this.form.getRawValue()); + } +} diff --git a/apps/frontend/src/features/communications/components/CommunicationStatusCard.module.css b/apps/frontend/src/app/features/assistant/components/communication-status-card.css similarity index 95% rename from apps/frontend/src/features/communications/components/CommunicationStatusCard.module.css rename to apps/frontend/src/app/features/assistant/components/communication-status-card.css index 5aaeff6..4cacd32 100644 --- a/apps/frontend/src/features/communications/components/CommunicationStatusCard.module.css +++ b/apps/frontend/src/app/features/assistant/components/communication-status-card.css @@ -36,3 +36,7 @@ .card p { color: var(--poc-muted); } + +:host { + display: contents; +} diff --git a/apps/frontend/src/features/assistant/components/GeneratedCommunicationPreview.module.css b/apps/frontend/src/app/features/assistant/components/generated-communication-preview.css similarity index 96% rename from apps/frontend/src/features/assistant/components/GeneratedCommunicationPreview.module.css rename to apps/frontend/src/app/features/assistant/components/generated-communication-preview.css index 1c12528..c2e0ddd 100644 --- a/apps/frontend/src/features/assistant/components/GeneratedCommunicationPreview.module.css +++ b/apps/frontend/src/app/features/assistant/components/generated-communication-preview.css @@ -52,3 +52,7 @@ .draft p { margin: 0; } + +:host { + display: contents; +} diff --git a/apps/frontend/src/app/features/assistant/components/generated-communication-preview.ts b/apps/frontend/src/app/features/assistant/components/generated-communication-preview.ts new file mode 100644 index 0000000..a716afc --- /dev/null +++ b/apps/frontend/src/app/features/assistant/components/generated-communication-preview.ts @@ -0,0 +1,42 @@ +import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; +import { EmptyStateComponent } from "../../../shared/components/empty-state/empty-state"; +import { StatusBadgeComponent } from "../../../shared/components/status-badge/status-badge"; +import { SectionComponent } from "../../../layout/section/section"; +import type { GeneratedDraft } from "../assistant.model"; + +@Component({ + selector: "poc-generated-communication-preview", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [EmptyStateComponent, SectionComponent, StatusBadgeComponent], + template: ` + + {{ bodyLength() }} caratteri + @if (draft(); as currentDraft) { +
+
+ Bozza generata +
+ + + +
+ } @else { + La bozza generata apparira qui. + } +
+ `, + styleUrl: "./generated-communication-preview.css" +}) +export class GeneratedCommunicationPreviewComponent { + readonly draft = input(null); + protected readonly bodyLength = computed(() => this.draft()?.body.length ?? 0); +} diff --git a/apps/frontend/src/app/features/assistant/data/assistant.service.ts b/apps/frontend/src/app/features/assistant/data/assistant.service.ts new file mode 100644 index 0000000..9b4c847 --- /dev/null +++ b/apps/frontend/src/app/features/assistant/data/assistant.service.ts @@ -0,0 +1,24 @@ +import { Injectable, inject } from "@angular/core"; +import { type Observable, tap } from "rxjs"; +import { AlittlebytePoCAPIService } from "../../../../api/generated/poc-api"; +import type { GenerateCommunicationResponse } from "../../../../api/generated/model"; +import { PocStateStore } from "../../../core/state/poc-state.store"; +import type { CommunicationDraftForm } from "../assistant.model"; + +/** + * Generazione assistita di comunicazioni HR. La risposta contiene sia la bozza + * sia lo stato applicativo aggiornato: si rimpiazza lo store con quello + * autorevole del backend e si restituisce la risposta al componente per + * popolare anteprima e messaggio di stato. + */ +@Injectable({ providedIn: "root" }) +export class AssistantService { + private readonly api = inject(AlittlebytePoCAPIService); + private readonly store = inject(PocStateStore); + + generate(payload: CommunicationDraftForm): Observable { + return this.api + .generatePocCommunication(payload) + .pipe(tap((response) => this.store.setState(response.state))); + } +} diff --git a/apps/frontend/src/features/documents/components/DocumentList.module.css b/apps/frontend/src/app/features/copilot/components/document-list.css similarity index 86% rename from apps/frontend/src/features/documents/components/DocumentList.module.css rename to apps/frontend/src/app/features/copilot/components/document-list.css index 90b7f3e..af3b35b 100644 --- a/apps/frontend/src/features/documents/components/DocumentList.module.css +++ b/apps/frontend/src/app/features/copilot/components/document-list.css @@ -15,3 +15,7 @@ min-height: 34px; padding: 0 var(--poc-space-3); } + +:host { + display: contents; +} diff --git a/apps/frontend/src/app/features/copilot/components/document-list.ts b/apps/frontend/src/app/features/copilot/components/document-list.ts new file mode 100644 index 0000000..959ff31 --- /dev/null +++ b/apps/frontend/src/app/features/copilot/components/document-list.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; +import type { SubDocument } from "../../../../api/generated/model"; +import { ButtonComponent } from "../../../shared/components/button/button"; +import { EmptyStateComponent } from "../../../shared/components/empty-state/empty-state"; +import { StatusBadgeComponent } from "../../../shared/components/status-badge/status-badge"; +import { formatFallback } from "../../../shared/util/formatters"; +import { getReviewStatusTone } from "../../../shared/util/status"; + +@Component({ + selector: "poc-document-list", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ButtonComponent, EmptyStateComponent, StatusBadgeComponent], + template: ` + @if (documents().length) { +
+ + + + + + + + + + + + + + @for (documentItem of documents(); track documentItem.id) { + + + + + + + + + + } + +
DestinatarioDocumentoFileDataConf.StatoAzioni
+ + {{ formatFallback(documentItem.employee, "Destinatario rilevato") }} + {{ formatFallback(documentItem.company) }} + + + {{ formatFallback(documentItem.title || documentItem.type, "Documento") }} + {{ formatFallback(documentItem.file) }} + {{ formatFallback(documentItem.date) }} + + {{ formatFallback(documentItem.confidence, "Da verificare") }} + + + {{ documentItem.reviewStatusLabel }} + + + +
+
+ } @else { + I documenti caricati compariranno qui. + } + `, + styleUrls: ["../../../shared/components/data-table/data-table.css", "./document-list.css"] +}) +export class DocumentListComponent { + readonly documents = input([]); + readonly selectedDocumentId = input(null); + readonly selectDocument = output(); + + protected readonly formatFallback = formatFallback; + protected readonly getReviewStatusTone = getReviewStatusTone; +} diff --git a/apps/frontend/src/features/documents/components/DocumentStatusTimeline.module.css b/apps/frontend/src/app/features/copilot/components/document-status-timeline.css similarity index 79% rename from apps/frontend/src/features/documents/components/DocumentStatusTimeline.module.css rename to apps/frontend/src/app/features/copilot/components/document-status-timeline.css index 2beb716..309e5a1 100644 --- a/apps/frontend/src/features/documents/components/DocumentStatusTimeline.module.css +++ b/apps/frontend/src/app/features/copilot/components/document-status-timeline.css @@ -6,3 +6,7 @@ padding: 0; list-style: none; } + +:host { + display: contents; +} diff --git a/apps/frontend/src/app/features/copilot/components/document-status-timeline.ts b/apps/frontend/src/app/features/copilot/components/document-status-timeline.ts new file mode 100644 index 0000000..5001c3e --- /dev/null +++ b/apps/frontend/src/app/features/copilot/components/document-status-timeline.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component, input } from "@angular/core"; +import type { SubDocument } from "../../../../api/generated/model"; +import { StatusBadgeComponent } from "../../../shared/components/status-badge/status-badge"; +import { getReviewStatusTone } from "../../../shared/util/status"; + +@Component({ + selector: "poc-document-status-timeline", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [StatusBadgeComponent], + template: ` +
    +
  • + + {{ documentItem().reviewStatusLabel }} + +
  • +
+ `, + styleUrl: "./document-status-timeline.css" +}) +export class DocumentStatusTimelineComponent { + readonly documentItem = input.required(); + protected readonly getReviewStatusTone = getReviewStatusTone; +} diff --git a/apps/frontend/src/features/documents/components/DocumentUploadPanel.module.css b/apps/frontend/src/app/features/copilot/components/document-upload-panel.css similarity index 79% rename from apps/frontend/src/features/documents/components/DocumentUploadPanel.module.css rename to apps/frontend/src/app/features/copilot/components/document-upload-panel.css index 88bbe93..582057d 100644 --- a/apps/frontend/src/features/documents/components/DocumentUploadPanel.module.css +++ b/apps/frontend/src/app/features/copilot/components/document-upload-panel.css @@ -8,3 +8,7 @@ margin: 0; color: var(--poc-muted); } + +:host { + display: contents; +} diff --git a/apps/frontend/src/app/features/copilot/components/document-upload-panel.ts b/apps/frontend/src/app/features/copilot/components/document-upload-panel.ts new file mode 100644 index 0000000..e1331f8 --- /dev/null +++ b/apps/frontend/src/app/features/copilot/components/document-upload-panel.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; +import { FileDropzoneComponent } from "../../../shared/components/file-dropzone/file-dropzone"; +import { SectionComponent } from "../../../layout/section/section"; +import type { DocumentUploadPhase } from "../data/document-workflow.service"; +import { UploadProgressComponent } from "./upload-progress"; + +@Component({ + selector: "poc-document-upload-panel", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FileDropzoneComponent, SectionComponent, UploadProgressComponent], + template: ` + +

+ Il sistema analizza automaticamente il documento dopo il caricamento e restituisce i campi rilevati + nella verifica. +

+ + @if (phase() !== null) { + + } +

{{ status() }}

+
+ `, + styleUrl: "./document-upload-panel.css" +}) +export class DocumentUploadPanelComponent { + readonly isUploading = input.required(); + readonly status = input.required(); + readonly phase = input.required(); + readonly upload = output(); +} diff --git a/apps/frontend/src/features/documents/components/SubDocumentList.module.css b/apps/frontend/src/app/features/copilot/components/sub-document-list.css similarity index 79% rename from apps/frontend/src/features/documents/components/SubDocumentList.module.css rename to apps/frontend/src/app/features/copilot/components/sub-document-list.css index 013afa9..24e80b3 100644 --- a/apps/frontend/src/features/documents/components/SubDocumentList.module.css +++ b/apps/frontend/src/app/features/copilot/components/sub-document-list.css @@ -62,13 +62,17 @@ .previewLink { margin-top: var(--poc-space-1); min-height: 38px; - display: inline-flex; + display: flex; align-items: center; + justify-content: center; + width: 100%; + justify-self: stretch; padding: 8px 12px; border-radius: var(--poc-radius); background: var(--poc-surface-soft); color: var(--poc-primary-deep); font-weight: 700; + text-align: center; text-decoration: none; } @@ -79,22 +83,34 @@ .previewLines { display: grid; - gap: var(--poc-space-2); - margin: 0; - padding: 0; + gap: 2px; + margin: var(--poc-space-1) 0 0; + padding: var(--poc-space-3) 0 0; + border-top: 1px solid var(--poc-border-soft); list-style: none; } .previewLines li { - padding: 9px 11px; - border: 1px solid var(--poc-border-soft); - border-radius: var(--poc-radius); - background: var(--poc-surface); + position: relative; + padding: 5px 0 5px 16px; color: var(--poc-muted); - font-size: var(--poc-font-sm); + font-size: var(--poc-font-xs); + line-height: 1.45; overflow-wrap: anywhere; } +.previewLines li::before { + content: ""; + position: absolute; + left: 3px; + top: 11px; + width: 5px; + height: 5px; + border-radius: 999px; + background: var(--poc-primary); + opacity: 0.55; +} + .errorNote { margin: 0; padding: 10px 12px; @@ -191,8 +207,8 @@ .editableField input[readonly], .editableField textarea[readonly] { - border-color: #c9e5d3; - background: var(--poc-success-soft); + border-color: var(--poc-border-soft); + background: var(--poc-surface-muted); } .confidenceLow .editableField input[readonly], @@ -201,6 +217,21 @@ background: var(--poc-warning-soft); } +/* Validato automaticamente (alta confidenza): campi in azzurro. */ +.statusAuto .editableField input[readonly], +.statusAuto .editableField textarea[readonly] { + border-color: var(--poc-primary); + background: rgba(121, 173, 211, 0.16); +} + +/* Validato/corretto manualmente: campi in verde. Ultima regola a pari + specificita': la conferma umana prevale anche su una confidenza bassa. */ +.statusManual .editableField input[readonly], +.statusManual .editableField textarea[readonly] { + border-color: #c9e5d3; + background: var(--poc-success-soft); +} + .lockedField input, .lockedField textarea { border-color: var(--poc-border-soft); @@ -232,6 +263,9 @@ flex-wrap: wrap; gap: var(--poc-space-2); justify-content: flex-end; + margin-top: var(--poc-space-1); + padding-top: var(--poc-space-3); + border-top: 1px solid var(--poc-border-soft); } @media (max-width: 760px) { @@ -248,3 +282,7 @@ justify-content: stretch; } } + +:host { + display: contents; +} diff --git a/apps/frontend/src/app/features/copilot/components/sub-document-list.ts b/apps/frontend/src/app/features/copilot/components/sub-document-list.ts new file mode 100644 index 0000000..0ca07a5 --- /dev/null +++ b/apps/frontend/src/app/features/copilot/components/sub-document-list.ts @@ -0,0 +1,377 @@ +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, signal } from "@angular/core"; +import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms"; +import { DomSanitizer, type SafeResourceUrl } from "@angular/platform-browser"; +import { LucideCheckCircle2, LucidePencil, LucideSave, LucideTrash2, LucideX } from "@lucide/angular"; +import type { SubDocument, UpdateExtractedDataRequest } from "../../../../api/generated/model"; +import { ButtonComponent } from "../../../shared/components/button/button"; +import { EmptyStateComponent } from "../../../shared/components/empty-state/empty-state"; +import { SectionComponent } from "../../../layout/section/section"; +import { formatDateForDisplay, formatFallback } from "../../../shared/util/formatters"; +import { DocumentWorkflowService, type DocumentPreviewStatus } from "../data/document-workflow.service"; +import { DocumentStatusTimelineComponent } from "./document-status-timeline"; + +interface ReviewFormState { + employeeName: string; + employeeFirstName: string; + employeeLastName: string; + companyName: string; + documentDate: string; + documentType: string; + description: string; +} + +const emptyReviewForm: ReviewFormState = { + employeeName: "", + employeeFirstName: "", + employeeLastName: "", + companyName: "", + documentDate: "", + documentType: "", + description: "" +}; + +@Component({ + selector: "poc-sub-document-list", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ButtonComponent, + DocumentStatusTimelineComponent, + EmptyStateComponent, + LucideCheckCircle2, + LucidePencil, + LucideSave, + LucideTrash2, + LucideX, + ReactiveFormsModule, + SectionComponent + ], + template: ` + @if (documentItem(); as document) { + + + + + +
+
+

Anteprima documento

+
+ {{ formatFallback(document.file, "Documento") }} + @if (previewStatus() === "available" && previewSrc(); as src) { + + + Apri in una nuova scheda + + } @else if (previewStatus() === "loading") { + Caricamento anteprima in corso. + } @else if (previewStatus() === "unreachable") { + Storage documento non raggiungibile. + } @else if (previewStatus() === "unavailable") { + Anteprima non disponibile. + } @else { + Anteprima non ancora disponibile per questo documento. + } +
+ @if (document.previewLines.length) { +
    + @for (line of document.previewLines; track line) { +
  • {{ line }}
  • + } +
+ } +
+ +
+
+

Dati estratti dall'OCR

+
+ Modificabile + Sola lettura +
+
+ @if (document.error) { +

{{ document.error }}

+ } + @if (reviewError()) { +

{{ reviewError() }}

+ } + +
+
+ + + + + + + + + +
+ +
+ @if (isEditing()) { + + + + } @else { + + + } +
+
+
+
+
+ } @else { + + Seleziona un documento dallo storico per visualizzare il dettaglio. + + } + `, + styleUrl: "./sub-document-list.css" +}) +export class SubDocumentListComponent { + readonly documentItem = input(null); + readonly isDeleting = input.required(); + readonly isSavingReview = input.required(); + readonly reviewError = input(null); + readonly deleteDocument = output(); + readonly markReviewed = output(); + readonly saveReviewRequested = output<{ documentId: string; payload: UpdateExtractedDataRequest }>(); + + protected readonly formatFallback = formatFallback; + protected readonly isEditing = signal(false); + protected readonly previewStatus = signal("idle"); + // L'URL dell'anteprima e' una risorsa same-origin generata dal backend; va + // marcato come SafeResourceUrl, altrimenti Angular blocca il binding su + //