diff --git a/.env.example b/.env.example index c661be4..8006cdd 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 + CDN/edge 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 +EDGE_CDN_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..97f0541 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 -> edge-cdn -> 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 edge-cdn, + # 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; +* **emulatore CDN locale** (Nginx) davanti al bucket S3 LocalStack per il serving della SPA Angular (in produzione: AWS CloudFront); * 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 l’emulatore CDN locale. Quest’ultimo (`edge-cdn`) è un secondo Nginx che emula il ruolo di una CDN/edge — non Amazon CloudFront — servendo la SPA Angular dagli oggetti caricati nel bucket S3 LocalStack e inoltrando `/api/`, `/health` e `/ready` all’Nginx applicativo/Laravel. È un container separato dall’Nginx applicativo, che è un’immagine di produzione e non deve conoscere LocalStack; quest’ultimo resta il proxy 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 edge-cdn-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 l’emulatore CDN locale su `https://localhost:8443`. L’emulazione valida il pattern build → bucket → distribuzione edge in locale, ma non sostituisce una CDN reale (in produzione AWS CloudFront, con TLS/OAC/edge propagation/invalidation). + +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..b48aa86 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, in the default local flow, by the local CDN emulator (a separate Nginx) in front of the LocalStack S3 bucket — the role a real CDN such as AWS CloudFront would play in production. 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 + //