chore: remove historical archive and refresh repository docs
Remove old_repo now that the rebuilt platform is the active implementation. Point documentation to current code and Git history, preserve local environment setup guidance, and drop obsolete archive exclusions from build and quality tools.
@ -1,5 +1,4 @@
|
|||||||
# Never send the archived codebase or local configuration to a build context.
|
# Never send local configuration to a build context.
|
||||||
old_repo/
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
exclude: ^old_repo/
|
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.11
|
rev: v0.12.11
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Repository workflow
|
# Repository workflow
|
||||||
|
|
||||||
This repository is the rebuilt AI Platform. `old_repo/` is historical and excluded from builds. The approved rebuild plan authorizes implementation; `docs/specs/ai-platform-lead-scoring.md` is review-ready for cross-service acceptance, so unverified Omni/AIHub wire details are not release claims.
|
This repository is the rebuilt AI Platform. Historical implementation is retained only in Git history; current code lives under `src/ai_platform/` and `apps/web/`. The approved rebuild plan authorizes implementation; `docs/specs/ai-platform-lead-scoring.md` is review-ready for cross-service acceptance, so unverified Omni/AIHub wire details are not release claims.
|
||||||
|
|
||||||
1. Before changing Lead Scoring behavior, read `docs/specs/ai-platform-lead-scoring.md`, `CONTEXT.md`, and `ARCHITECTURE.md`. Read `stack.md` when choosing a runtime or dependency. Generated OpenAPI and small Omni fixtures own exact wire shapes.
|
1. Before changing Lead Scoring behavior, read `docs/specs/ai-platform-lead-scoring.md`, `CONTEXT.md`, and `ARCHITECTURE.md`. Read `stack.md` when choosing a runtime or dependency. Generated OpenAPI and small Omni fixtures own exact wire shapes.
|
||||||
2. Put app/admin authentication, configuration and shared DB setup in `platform`; put deterministic rules/metrics and run lifecycle in `lead_scoring/domain`; orchestration in `application`, I/O in `infrastructure`, HTTP mapping in `presentation`, and wiring in `bootstrap`. Churn follows the same layout only when a specification exists.
|
2. Put app/admin authentication, configuration and shared DB setup in `platform`; put deterministic rules/metrics and run lifecycle in `lead_scoring/domain`; orchestration in `application`, I/O in `infrastructure`, HTTP mapping in `presentation`, and wiring in `bootstrap`. Churn follows the same layout only when a specification exists.
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status and scope
|
## Status and scope
|
||||||
|
|
||||||
This document records boundaries for the approved AI Platform rebuild and app-auth baseline `omni-ai-platform-app-auth` revision 2. The local implementation is covered by provider checks documented in `docs/contracts/app-auth-provider-handoff.md`; cross-service and production acceptance remain pending. The root Lead Scoring provider specification `docs/specs/ai-platform-lead-scoring.md` remains `review_ready` for that acceptance. The archived code and documents under `old_repo/` are historical evidence, not the new architecture or implementation.
|
This document records boundaries for the approved AI Platform rebuild and app-auth baseline `omni-ai-platform-app-auth` revision 2. The local implementation is covered by provider checks documented in `docs/contracts/app-auth-provider-handoff.md`; cross-service and production acceptance remain pending. The root Lead Scoring provider specification `docs/specs/ai-platform-lead-scoring.md` remains `review_ready` for that acceptance. Historical implementation is available in Git history only; the current backend and Admin Web live under `src/ai_platform/` and `apps/web/`.
|
||||||
|
|
||||||
AI Platform is a provider service: it executes bounded rule scoring and a selected AI profile, maintains scoring-run lifecycle, and exposes result retrieval. DotB-Omni owns customer-configured rules/weights, HITL and customer actions, snapshot construction, user authorization, and CRM writeback. Omni is notified of terminal outcome through an agreed signed HMAC webhook and then calls the authenticated GET endpoint for run details. DotB EMS remains the CRM system of record. There is no customer-facing settings or review UI in this service scope.
|
AI Platform is a provider service: it executes bounded rule scoring and a selected AI profile, maintains scoring-run lifecycle, and exposes result retrieval. DotB-Omni owns customer-configured rules/weights, HITL and customer actions, snapshot construction, user authorization, and CRM writeback. Omni is notified of terminal outcome through an agreed signed HMAC webhook and then calls the authenticated GET endpoint for run details. DotB EMS remains the CRM system of record. There is no customer-facing settings or review UI in this service scope.
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Context
|
# Context
|
||||||
|
|
||||||
Canonical language for the AI Platform Lead Scoring provider. The approved rebuild and app-auth baseline `omni-ai-platform-app-auth` revision 2 are implemented and covered by local provider tests. The provider specification `docs/specs/ai-platform-lead-scoring.md` remains `review_ready` for cross-service acceptance. Local checks do not establish Omni/EMS integration or production readiness; see `docs/contracts/app-auth-provider-handoff.md` for evidence and remaining checks.
|
Canonical language for the AI Platform Lead Scoring provider. The approved rebuild and app-auth baseline `omni-ai-platform-app-auth` revision 2 are implemented and covered by local provider tests. The provider specification `docs/specs/ai-platform-lead-scoring.md` remains `review_ready` for cross-service acceptance. Local checks do not establish Omni/EMS integration or production readiness; see `docs/contracts/app-auth-provider-handoff.md` for evidence and remaining checks. Current implementation is under `src/ai_platform/` and `apps/web/`; historical code and contracts are available in Git history only.
|
||||||
|
|
||||||
## Systems and ownership
|
## Systems and ownership
|
||||||
|
|
||||||
|
|||||||
@ -8,13 +8,13 @@ AI Platform provides Lead Scoring with a Python backend and internal SvelteKit A
|
|||||||
- PostgreSQL 16 with a new, project-specific Compose volume named `ai_platform_pgdata`.
|
- PostgreSQL 16 with a new, project-specific Compose volume named `ai_platform_pgdata`.
|
||||||
- Alembic schema changes are applied as a one-shot Compose service before the backend starts.
|
- Alembic schema changes are applied as a one-shot Compose service before the backend starts.
|
||||||
- SvelteKit is a separate web image under `apps/web`.
|
- SvelteKit is a separate web image under `apps/web`.
|
||||||
- `old_repo/` is archived source only and is excluded from Docker build contexts and local checks.
|
- Historical implementation is retained in Git history, not as a separate source tree.
|
||||||
|
|
||||||
The local Compose stack runs one backend process. It is for development only; default credentials and loopback port mappings are not deployment settings. Keep production secrets outside `.env` and inject them through the deployment environment or a secrets manager.
|
The local Compose stack runs one backend process. It is for development only; default credentials and loopback port mappings are not deployment settings. Keep production secrets outside `.env` and inject them through the deployment environment or a secrets manager.
|
||||||
|
|
||||||
## Local setup
|
## Local setup
|
||||||
|
|
||||||
Copy the backend/Compose template and the separate SvelteKit server-only template. Both `.env` files are ignored by Git; the previous root `.env` is preserved under `old_repo/.env` as historical local data, not loaded by the rebuilt service.
|
Copy the backend/Compose template and the separate SvelteKit server-only template only when setting up a new workspace. Both `.env` files are ignored by Git; preserve any existing `.env` and `apps/web/.env` instead of overwriting them.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
@ -45,7 +45,7 @@ Compose can instead build and start all services, including the one-shot migrati
|
|||||||
docker compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
The database is reachable from the host at `127.0.0.1:55432`, backend at `127.0.0.1:8000`, and web at `127.0.0.1:3000` by default. To discard only this new database and its data, run `docker compose down -v`; this does not target archived or unrelated volumes.
|
The database is reachable from the host at `127.0.0.1:55432`, backend at `127.0.0.1:8000`, and web at `127.0.0.1:3000` by default. To discard this Compose stack's database and its data, run `docker compose down -v`; unrelated project volumes are not targeted.
|
||||||
|
|
||||||
The local database starts with only the development database. To run tests safely, create a separate database in that same new PostgreSQL instance, then pass its URL explicitly:
|
The local database starts with only the development database. To run tests safely, create a separate database in that same new PostgreSQL instance, then pass its URL explicitly:
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
# Local development only. Replace all credentials in deployed environments.
|
|
||||||
ENVIRONMENT=development
|
|
||||||
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5433/lead_scoring
|
|
||||||
AIHUB_BASE_URL=http://localhost:8080
|
|
||||||
DEBUG=false
|
|
||||||
|
|
||||||
# JSON arrays consumed by pydantic-settings. These values work with the Dev Console example.
|
|
||||||
SERVICE_CALLERS=[{"caller_id":"dev-console","token":"local-dev-token","allowed_apps":["dev-console"],"allowed_features":["lead-scoring"],"allowed_tenants":["omni-account-42","omni-test-tenant-42"]}]
|
|
||||||
TENANT_ALLOWLIST=["omni-account-42","omni-test-tenant-42"]
|
|
||||||
SCORING_CONFIGS=[{"version":"1.0.0","confidence":0.8,"confidence_threshold":0.7,"hitl_policy_version":"1.0.0","effort_profile":"medium","fallback_policy":"rules_only_review","ai_model":"openai/gpt-4o-mini"}]
|
|
||||||
|
|
||||||
WORKER_LEASE_SECONDS=30
|
|
||||||
WORKER_RECOVERY_INTERVAL_SECONDS=5
|
|
||||||
KILL_SWITCH_ENABLED=false
|
|
||||||
|
|
||||||
# Enable webhook delivery by setting both values.
|
|
||||||
# WEBHOOK_SUBSCRIBER_URL=http://localhost:8080/api/v1/internal/lead-scoring/events
|
|
||||||
# WEBHOOK_SIGNING_SECRET=replace-with-local-signing-secret
|
|
||||||
WEBHOOK_SIGNING_KEY_ID=pilot-2026-01
|
|
||||||
76
old_repo/.gitignore
vendored
@ -1,76 +0,0 @@
|
|||||||
# Python & Virtual Environments
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
*.so
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env/
|
|
||||||
.uv/
|
|
||||||
|
|
||||||
# Linters, Type Checkers & Test Caches
|
|
||||||
.ruff_cache/
|
|
||||||
.pytest_cache/
|
|
||||||
.mypy_cache/
|
|
||||||
.pyre/
|
|
||||||
.pytype/
|
|
||||||
.pyright/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
htmlcov/
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
|
|
||||||
# Node / Bun / Frontend
|
|
||||||
node_modules/
|
|
||||||
node_modules
|
|
||||||
apps/dev-console/node_modules
|
|
||||||
.bun/
|
|
||||||
dist-ssr/
|
|
||||||
*.local
|
|
||||||
.svelte-kit/
|
|
||||||
|
|
||||||
# Playwright & Testing Reports
|
|
||||||
playwright-report/
|
|
||||||
test-results/
|
|
||||||
blob-report/
|
|
||||||
|
|
||||||
# Environment Variables & Local Secrets
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Project-local
|
|
||||||
vault/
|
|
||||||
.scratch/
|
|
||||||
.codex/
|
|
||||||
docs/agents/
|
|
||||||
.claude/
|
|
||||||
CLAUDE.LOCAL.md
|
|
||||||
# Generated artifacts
|
|
||||||
/openapi.json
|
|
||||||
|
|
||||||
# IDE, OS & Editor Artifacts
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
@ -1 +0,0 @@
|
|||||||
3.13
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
This file provides guidance to Codex (Codex.ai/code) when working with this repository.
|
|
||||||
|
|
||||||
## Purpose and status
|
|
||||||
|
|
||||||
This repository is the Python-first AI Platform workspace. Lead Scoring is the first service; related systems are `DotB-Omni`, `lead-scoring-agent`, and `aihub` under `/home/hung/code/dotb_workspace/`.
|
|
||||||
|
|
||||||
The repository contains specifications, project documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`).
|
|
||||||
|
|
||||||
Before implementing or changing Lead Scoring Customer Pilot behavior, read:
|
|
||||||
|
|
||||||
- `docs/specs/lead-scoring-customer-pilot.md`: authoritative product behavior, scope, invariants, acceptance boundaries, and cross-service ownership.
|
|
||||||
- `stack.md`: technology decisions, candidates, evidence, and unresolved stack questions. It does not override the approved specification.
|
|
||||||
|
|
||||||
The DotB Vault RFC, initial brief, Grill Brief, legacy reports, drafts, and agent messages are historical/supporting context rather than implementation authority. Exact wire formats become authoritative through released Pydantic models, generated OpenAPI/JSON Schemas, and shared fixtures.
|
|
||||||
|
|
||||||
## Communication
|
|
||||||
|
|
||||||
- Keep responses short, specific, and decision-oriented.
|
|
||||||
- Put the decision or conclusion first, followed by only the evidence and next action needed.
|
|
||||||
- Keep the main context high-signal. Put raw logs, scratch data, and long intermediate reports in `/tmp/` or the appropriate durable document.
|
|
||||||
- Use lists and compact structure when they make the result easier to scan.
|
|
||||||
- State assumptions, unresolved decisions, and verification status explicitly.
|
|
||||||
|
|
||||||
## Writing
|
|
||||||
|
|
||||||
- Write directly and precisely. Avoid buzzwords, filler, and repeated explanations.
|
|
||||||
- Comments explain intent, invariants, trade-offs, or non-obvious constraints, not syntax or line-by-line behavior.
|
|
||||||
- Do not add speculative documentation, abstractions, or metadata that the repository does not need.
|
|
||||||
- Do not use emoji or em dashes in new prose, comments, or reports. Preserve emoji and punctuation that are part of existing paths or quoted source text.
|
|
||||||
|
|
||||||
## Delegation
|
|
||||||
|
|
||||||
- Prioritize standard subagents over teammates to minimize token overhead. Spawn multiple subagents when parallel execution is genuinely needed.
|
|
||||||
- Pick the agent type matching the task from available configured agents (e.g., `explorer`, `implementer`, `tester`, `researcher`, `adversarial-reviewer`, `general`, `codebase-memory`).
|
|
||||||
- Delegate context-heavy or tool-heavy work with a clear scope, completion criteria, and evidence requirements.
|
|
||||||
- Require condensed handoffs containing conclusions, changed paths, verification, and uncertainty. Do not paste raw logs into the main context.
|
|
||||||
- The main context retains final decisions and integration responsibility. Agent messages are evidence, not user approval.
|
|
||||||
|
|
||||||
## Agent skills
|
|
||||||
|
|
||||||
### Issue tracker
|
|
||||||
|
|
||||||
Local Markdown issues are stored under `.scratch/<feature-slug>/`. See `docs/agents/issue-tracker.md`.
|
|
||||||
|
|
||||||
### Domain docs
|
|
||||||
|
|
||||||
The repository uses a single-context domain layout. See `docs/agents/domain.md`.
|
|
||||||
|
|
||||||
## Project knowledge
|
|
||||||
|
|
||||||
- `AGENTS.md` is the project map for conventions, verified commands, boundaries, and gotchas.
|
|
||||||
- `docs/specs/lead-scoring-customer-pilot.md` is the approved source of truth for Lead Scoring Customer Pilot product behavior and build boundaries.
|
|
||||||
- `stack.md` is the primary record for technology decisions and unresolved stack questions; it cannot override the approved spec.
|
|
||||||
- `docs/` contains shared project knowledge intended for version control.
|
|
||||||
- `/tmp/` is for scratch data, raw logs, and temporary handoffs.
|
|
||||||
- Released Pydantic models, generated OpenAPI/JSON Schemas, tests, and shared fixtures become the exact executable contract when they exist.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- Put temporary reports, raw logs, and scratch material in `/tmp/`.
|
|
||||||
- Put durable team knowledge in `docs/` and include it in version control.
|
|
||||||
- Use `vault/` only for durable project-local knowledge that must not be committed.
|
|
||||||
- New durable Markdown documents under `docs/` or `vault/` use minimal YAML frontmatter:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
title: Document title
|
|
||||||
description: A short description of what the document contains and when it is useful.
|
|
||||||
status: draft
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
- `title` and `description` are required. For governed specs, `status` may be `draft`, `review_ready`, `approved`, or `deprecated`; other existing document schemas keep their established status vocabulary. `tags` is optional.
|
|
||||||
- Do not add metadata that Git, the filesystem, or existing tooling can derive.
|
|
||||||
- Preserve the format and metadata schema of existing documents. Do not add frontmatter, reformat, move, or normalize existing files unless the task explicitly requests migration.
|
|
||||||
- `AGENTS.md`, README files, and files with an existing schema keep their established format.
|
|
||||||
|
|
||||||
## Repository structure
|
|
||||||
|
|
||||||
- `src/lead_scoring/`: Lead Scoring production service package and CLI tools.
|
|
||||||
- `alembic/`: database migrations for PostgreSQL.
|
|
||||||
- `tests/`: test suite including health check, OpenAPI CLI, and migration lifecycle.
|
|
||||||
- `docs/specs/lead-scoring-customer-pilot.md`: approved Lead Scoring Customer Pilot specification.
|
|
||||||
- `docs/`: durable shared project documentation.
|
|
||||||
- `.scratch/`: local untracked draft specs and issue tickets used by agent skills.
|
|
||||||
- `stack.md`: technology decisions and unresolved stack questions.
|
|
||||||
- `.codex/settings.json`: project hook configuration.
|
|
||||||
- `.codex/hooks/post-edit-check.py`: Python-only PostToolUse feedback hook.
|
|
||||||
|
|
||||||
## Product and frontend boundary
|
|
||||||
|
|
||||||
The frontend boundary is approved for Customer Pilot: DotB-Omni owns all customer-facing Lead Scoring configuration and HITL UI. AI Platform may provide only an internal Dev Console for fixtures, contract testing, failure testing, and allowlisted integration verification; it must not duplicate Omni business UI.
|
|
||||||
|
|
||||||
When the internal Dev Console is implemented:
|
|
||||||
|
|
||||||
- Use Svelte 5 and TypeScript with Bun, Vite, and Vitest. Add SvelteKit only when routing, SSR, or deployment requires it.
|
|
||||||
- Generate types from backend OpenAPI and consume them through `openapi-fetch`.
|
|
||||||
- Keep shared transport concerns such as authentication and error handling outside generated code.
|
|
||||||
- Use the official Svelte tooling, including `@sveltejs/mcp` autofixer feedback and `svelte-check`.
|
|
||||||
- Use Playwright only for important end-to-end user flows.
|
|
||||||
|
|
||||||
TypeScript 7 native compiler and language server are pilot candidates, not the adopted project default. Test them project-locally before changing global runtimes or the shared `mcpls` configuration.
|
|
||||||
|
|
||||||
## Engineering principles
|
|
||||||
|
|
||||||
- Follow YAGNI. Implement only what the current requirement needs; do not add speculative abstractions, extension points, compatibility layers, or infrastructure.
|
|
||||||
- Prefer the simplest design that preserves clear boundaries and can be changed safely.
|
|
||||||
- Reuse the standard library and established, actively maintained packages when they reduce code and operational burden. Avoid dependencies for trivial behavior.
|
|
||||||
- Keep code explicit. Use precise names, short control flow, and direct data transformations. Avoid cleverness, hidden side effects, global mutable state, and premature optimization.
|
|
||||||
- Prefer composition over inheritance and functions over classes. Introduce a class only when state, lifecycle, or a framework contract genuinely requires one.
|
|
||||||
- Keep a functional core with side effects at the boundaries. Isolate I/O, persistence, network calls, clocks, and environment access.
|
|
||||||
- Organize modules by capability or domain responsibility, not by arbitrary technical layers.
|
|
||||||
- Avoid wrappers that only rename another API. Every abstraction must remove meaningful complexity or enforce a real boundary.
|
|
||||||
- Preserve one clear source of truth for contracts, configuration, and domain rules.
|
|
||||||
|
|
||||||
## AI-native code
|
|
||||||
|
|
||||||
Write code that humans and coding agents can understand with minimal hidden context:
|
|
||||||
|
|
||||||
- Make dependencies, inputs, outputs, errors, and side effects explicit.
|
|
||||||
- Use typed, stable contracts at module and service boundaries.
|
|
||||||
- Keep related implementation and tests easy to locate.
|
|
||||||
- Prefer deterministic behavior and small composable functions.
|
|
||||||
- Document decisions and non-obvious constraints, not syntax or line-by-line behavior.
|
|
||||||
- Match existing conventions before introducing a new pattern.
|
|
||||||
- Make the smallest coherent change and remove superseded code instead of leaving parallel implementations.
|
|
||||||
|
|
||||||
## Verified commands
|
|
||||||
|
|
||||||
- `uv sync`: sync workspace dependencies and environment.
|
|
||||||
- `uv run pytest`: run pytest test suite against Python 3.13.
|
|
||||||
- `uv run ruff check src tests`: run lint checks across source and tests.
|
|
||||||
- `uv run ruff format --check src tests`: check code formatting.
|
|
||||||
- `uv run pyright`: run Pyright static type checker.
|
|
||||||
- `uv run ty check`: run project-local ty type checker.
|
|
||||||
- `uv run generate-openapi`: deterministically generate root `openapi.json`.
|
|
||||||
- `uv run generate-openapi --check`: verify that `openapi.json` is present and matches the current backend contract.
|
|
||||||
- `uv run generate-webhook-contract`: deterministically generate terminal webhook contract pack.
|
|
||||||
- `uv run generate-webhook-contract --check`: verify that webhook contract pack is present and matches current models.
|
|
||||||
- `uv run generate-polling-contract`: deterministically generate scoring run polling contract pack.
|
|
||||||
- `uv run generate-polling-contract --check`: verify that polling contract pack is present and matches current models.
|
|
||||||
- `uv run generate-contract-pack`: deterministically generate the versioned shared Contract Pack.
|
|
||||||
- `uv run generate-contract-pack --validate-fixtures`: validate provider and consumer fixture expectations.
|
|
||||||
- `uv run generate-contract-pack --check`: verify the versioned Contract Pack and checksums are present and current.
|
|
||||||
- `uv run alembic upgrade head`: apply database migrations up to head.
|
|
||||||
- `uv run alembic downgrade base`: revert all database migrations.
|
|
||||||
|
|
||||||
## Deterministic generation
|
|
||||||
|
|
||||||
- When a verified project generator exists, use it instead of manually editing derived files.
|
|
||||||
- Treat OpenAPI output and generated frontend types as derived artifacts. Regenerate them from the backend contract and require the authoritative verification command to detect drift.
|
|
||||||
- Do not edit generated files manually unless the project workflow explicitly requires it.
|
|
||||||
|
|
||||||
## Safety and scope
|
|
||||||
|
|
||||||
- Inspect a path before overwriting or deleting it. Preserve existing configuration, documentation, code, and data unless the task explicitly authorizes removal or migration.
|
|
||||||
- Do not expose secrets in logs, reports, handoffs, or generated artifacts.
|
|
||||||
- Do not install packages, modify system configuration, or create permissions and background services without explicit approval.
|
|
||||||
- Bootstrap production code only after the relevant product and implementation decision. Customer-facing Lead Scoring UI remains owned by DotB-Omni.
|
|
||||||
|
|
||||||
## Tooling and verification gotchas
|
|
||||||
|
|
||||||
- Root `openapi.json` and frontend build output remain covered by the ignore policy; the versioned Contract Pack OpenAPI artifact is generated and committed, and must not be edited manually.
|
|
||||||
- The project PostToolUse hook is configured in `.codex/settings.json` for `Write` and `Edit`. It handles Python files via `ruff` (safe fix and format). Full type-checking (`pyright`, `ty`) and test suites belong to explicit verification steps. Preserve the working-tree modification to `.codex/hooks/post-edit-check.py` unless explicitly asked to change or reset it.
|
|
||||||
- Do not modify `/etc`, package-manager state, system services, drivers, kernel settings, global Node/TypeScript versions, or the shared `mcpls` configuration as part of repository work unless explicitly approved.
|
|
||||||
|
|
||||||
## Quality of life
|
|
||||||
|
|
||||||
Pyright LSP is available for semantic navigation. Use it for definitions, references, hover types, document and workspace symbols, and call hierarchy; then read the relevant implementation to understand business rules and side effects.
|
|
||||||
@ -1,159 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with this repository.
|
|
||||||
|
|
||||||
## Purpose and status
|
|
||||||
|
|
||||||
This repository is the Python-first AI Platform workspace. Lead Scoring is the first service; related systems are `DotB-Omni`, `lead-scoring-agent`, and `aihub` under `/home/hung/code/dotb_workspace/`.
|
|
||||||
|
|
||||||
The repository contains specifications, project documentation, and the initialized baseline for the Lead Scoring service (`src/lead_scoring/`).
|
|
||||||
|
|
||||||
Before implementing or changing Lead Scoring Customer Pilot behavior, read:
|
|
||||||
|
|
||||||
- `docs/specs/lead-scoring-customer-pilot.md`: authoritative product behavior, scope, invariants, acceptance boundaries, and cross-service ownership.
|
|
||||||
- `stack.md`: technology decisions, candidates, evidence, and unresolved stack questions. It does not override the approved specification.
|
|
||||||
|
|
||||||
The DotB Vault RFC, initial brief, Grill Brief, legacy reports, drafts, and agent messages are historical/supporting context rather than implementation authority. Exact wire formats become authoritative through released Pydantic models, generated OpenAPI/JSON Schemas, and shared fixtures.
|
|
||||||
|
|
||||||
## Communication
|
|
||||||
|
|
||||||
- Keep responses short, specific, and decision-oriented.
|
|
||||||
- Put the decision or conclusion first, followed by only the evidence and next action needed.
|
|
||||||
- Keep the main context high-signal. Put raw logs, scratch data, and long intermediate reports in `/tmp/` or the appropriate durable document.
|
|
||||||
- Use lists and compact structure when they make the result easier to scan.
|
|
||||||
- State assumptions, unresolved decisions, and verification status explicitly.
|
|
||||||
|
|
||||||
## Writing
|
|
||||||
|
|
||||||
- Write directly and precisely. Avoid buzzwords, filler, and repeated explanations.
|
|
||||||
- Comments explain intent, invariants, trade-offs, or non-obvious constraints, not syntax or line-by-line behavior.
|
|
||||||
- Do not add speculative documentation, abstractions, or metadata that the repository does not need.
|
|
||||||
- Do not use emoji or em dashes in new prose, comments, or reports. Preserve emoji and punctuation that are part of existing paths or quoted source text.
|
|
||||||
|
|
||||||
## Delegation
|
|
||||||
|
|
||||||
- Use the configured `researcher` agent for read-only exploration and research.
|
|
||||||
- Use the configured `primary-engineer` agent for implementation, debugging, and multi-step engineering work. If it is unavailable, use the narrowest suitable implementation agent.
|
|
||||||
- Implementation agents own TDD tests; use the `tester` agent for independent verification before completion.
|
|
||||||
- Delegate context-heavy or tool-heavy work with a clear scope, completion criteria, and evidence requirements.
|
|
||||||
- Require condensed handoffs containing conclusions, changed paths, verification, and uncertainty. Do not paste raw logs into the main context.
|
|
||||||
- The main context retains final decisions and integration responsibility. Agent messages are evidence, not user approval.
|
|
||||||
|
|
||||||
## Project knowledge
|
|
||||||
|
|
||||||
- `CLAUDE.md` is the project map for conventions, verified commands, boundaries, and gotchas.
|
|
||||||
- `docs/specs/lead-scoring-customer-pilot.md` is the approved source of truth for Lead Scoring Customer Pilot product behavior and build boundaries.
|
|
||||||
- `stack.md` is the primary record for technology decisions and unresolved stack questions; it cannot override the approved spec.
|
|
||||||
- `docs/` contains shared project knowledge intended for version control.
|
|
||||||
- `/tmp/` is for scratch data, raw logs, and temporary handoffs.
|
|
||||||
- Released Pydantic models, generated OpenAPI/JSON Schemas, tests, and shared fixtures become the exact executable contract when they exist.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- Put temporary reports, raw logs, and scratch material in `/tmp/`.
|
|
||||||
- Put durable team knowledge in `docs/` and include it in version control.
|
|
||||||
- Use `vault/` only for durable project-local knowledge that must not be committed.
|
|
||||||
- New durable Markdown documents under `docs/` or `vault/` use minimal YAML frontmatter:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
title: Document title
|
|
||||||
description: A short description of what the document contains and when it is useful.
|
|
||||||
status: draft
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
- `title` and `description` are required. For governed specs, `status` may be `draft`, `review_ready`, `approved`, or `deprecated`; other existing document schemas keep their established status vocabulary. `tags` is optional.
|
|
||||||
- Do not add metadata that Git, the filesystem, or existing tooling can derive.
|
|
||||||
- Preserve the format and metadata schema of existing documents. Do not add frontmatter, reformat, move, or normalize existing files unless the task explicitly requests migration.
|
|
||||||
- `CLAUDE.md`, README files, and files with an existing schema keep their established format.
|
|
||||||
|
|
||||||
## Repository structure
|
|
||||||
|
|
||||||
- `src/lead_scoring/`: Lead Scoring production service package and CLI tools.
|
|
||||||
- `alembic/`: database migrations for PostgreSQL.
|
|
||||||
- `tests/`: test suite including health check, OpenAPI CLI, and migration lifecycle.
|
|
||||||
- `docs/specs/lead-scoring-customer-pilot.md`: approved Lead Scoring Customer Pilot specification.
|
|
||||||
- `docs/`: durable shared project documentation.
|
|
||||||
- `stack.md`: technology decisions and unresolved stack questions.
|
|
||||||
- `.claude/settings.json`: project hook configuration.
|
|
||||||
- `.claude/hooks/post-edit-check.py`: Python-only PostToolUse feedback hook.
|
|
||||||
|
|
||||||
## Product and frontend boundary
|
|
||||||
|
|
||||||
The frontend boundary is approved for Customer Pilot: DotB-Omni owns all customer-facing Lead Scoring configuration and HITL UI. AI Platform may provide only an internal Dev Console for fixtures, contract testing, failure testing, and allowlisted integration verification; it must not duplicate Omni business UI.
|
|
||||||
|
|
||||||
When the internal Dev Console is implemented:
|
|
||||||
|
|
||||||
- Use Svelte 5 and TypeScript with Bun, Vite, and Vitest. Add SvelteKit only when routing, SSR, or deployment requires it.
|
|
||||||
- Generate types from backend OpenAPI and consume them through `openapi-fetch`.
|
|
||||||
- Keep shared transport concerns such as authentication and error handling outside generated code.
|
|
||||||
- Use the official Svelte tooling, including `@sveltejs/mcp` autofixer feedback and `svelte-check`.
|
|
||||||
- Use Playwright only for important end-to-end user flows.
|
|
||||||
|
|
||||||
TypeScript 7 native compiler and language server are pilot candidates, not the adopted project default. Test them project-locally before changing global runtimes or the shared `mcpls` configuration.
|
|
||||||
|
|
||||||
## Engineering principles
|
|
||||||
|
|
||||||
- Follow YAGNI. Implement only what the current requirement needs; do not add speculative abstractions, extension points, compatibility layers, or infrastructure.
|
|
||||||
- Prefer the simplest design that preserves clear boundaries and can be changed safely.
|
|
||||||
- Reuse the standard library and established, actively maintained packages when they reduce code and operational burden. Avoid dependencies for trivial behavior.
|
|
||||||
- Keep code explicit. Use precise names, short control flow, and direct data transformations. Avoid cleverness, hidden side effects, global mutable state, and premature optimization.
|
|
||||||
- Prefer composition over inheritance and functions over classes. Introduce a class only when state, lifecycle, or a framework contract genuinely requires one.
|
|
||||||
- Keep a functional core with side effects at the boundaries. Isolate I/O, persistence, network calls, clocks, and environment access.
|
|
||||||
- Organize modules by capability or domain responsibility, not by arbitrary technical layers.
|
|
||||||
- Avoid wrappers that only rename another API. Every abstraction must remove meaningful complexity or enforce a real boundary.
|
|
||||||
- Preserve one clear source of truth for contracts, configuration, and domain rules.
|
|
||||||
|
|
||||||
## AI-native code
|
|
||||||
|
|
||||||
Write code that humans and coding agents can understand with minimal hidden context:
|
|
||||||
|
|
||||||
- Make dependencies, inputs, outputs, errors, and side effects explicit.
|
|
||||||
- Use typed, stable contracts at module and service boundaries.
|
|
||||||
- Keep related implementation and tests easy to locate.
|
|
||||||
- Prefer deterministic behavior and small composable functions.
|
|
||||||
- Document decisions and non-obvious constraints, not syntax or line-by-line behavior.
|
|
||||||
- Match existing conventions before introducing a new pattern.
|
|
||||||
- Make the smallest coherent change and remove superseded code instead of leaving parallel implementations.
|
|
||||||
|
|
||||||
## Verified commands
|
|
||||||
|
|
||||||
- `uv sync`: sync workspace dependencies and environment.
|
|
||||||
- `uv run pytest`: run pytest test suite against Python 3.13.
|
|
||||||
- `uv run ruff check src tests`: run lint checks across source and tests.
|
|
||||||
- `uv run ruff format --check src tests`: check code formatting.
|
|
||||||
- `uv run pyright`: run Pyright static type checker.
|
|
||||||
- `uv run ty check`: run project-local ty type checker.
|
|
||||||
- `uv run generate-openapi`: deterministically generate root `openapi.json`.
|
|
||||||
- `uv run generate-openapi --check`: verify that `openapi.json` is present and matches the current backend contract.
|
|
||||||
- `uv run generate-webhook-contract`: deterministically generate terminal webhook contract pack.
|
|
||||||
- `uv run generate-webhook-contract --check`: verify that webhook contract pack is present and matches current models.
|
|
||||||
- `uv run generate-polling-contract`: deterministically generate scoring run polling contract pack.
|
|
||||||
- `uv run generate-polling-contract --check`: verify that polling contract pack is present and matches current models.
|
|
||||||
- `uv run alembic upgrade head`: apply database migrations up to head.
|
|
||||||
- `uv run alembic downgrade base`: revert all database migrations.
|
|
||||||
|
|
||||||
## Deterministic generation
|
|
||||||
|
|
||||||
- When a verified project generator exists, use it instead of manually editing derived files.
|
|
||||||
- Treat OpenAPI output and generated frontend types as derived artifacts. Regenerate them from the backend contract and require the authoritative verification command to detect drift.
|
|
||||||
- Do not edit generated files manually unless the project workflow explicitly requires it.
|
|
||||||
|
|
||||||
## Safety and scope
|
|
||||||
|
|
||||||
- Inspect a path before overwriting or deleting it. Preserve existing configuration, documentation, code, and data unless the task explicitly authorizes removal or migration.
|
|
||||||
- Do not expose secrets in logs, reports, handoffs, or generated artifacts.
|
|
||||||
- Do not install packages, modify system configuration, or create permissions and background services without explicit approval.
|
|
||||||
- Bootstrap production code only after the relevant product and implementation decision. Customer-facing Lead Scoring UI remains owned by DotB-Omni.
|
|
||||||
|
|
||||||
## Tooling and verification gotchas
|
|
||||||
|
|
||||||
- Generated OpenAPI and frontend build output must remain covered by the existing ignore policy and should not be edited manually.
|
|
||||||
- The project PostToolUse hook is configured in `.claude/settings.json` for `Write` and `Edit`. It currently handles Python files only and invokes the existing `ruff` and `ty` tools. Preserve the pre-existing working-tree modification to `.claude/hooks/post-edit-check.py` unless explicitly asked to change or reset it.
|
|
||||||
- Do not modify `/etc`, package-manager state, system services, drivers, kernel settings, global Node/TypeScript versions, or the shared `mcpls` configuration as part of repository work unless explicitly approved.
|
|
||||||
|
|
||||||
## Quality of life
|
|
||||||
|
|
||||||
Pyright LSP is available for semantic navigation. Use it for definitions, references, hover types, document and workspace symbols, and call hierarchy; then read the relevant implementation to understand business rules and side effects.
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
# AI Platform - Lead Scoring Service
|
|
||||||
|
|
||||||
Lead Scoring service and internal Dev Console for the AI Platform.
|
|
||||||
|
|
||||||
## What runs locally
|
|
||||||
|
|
||||||
| Component | Purpose | Default address |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| PostgreSQL 16 | Stores scoring runs and migrations | `localhost:5433` |
|
|
||||||
| Lead Scoring API | REST API, async scoring worker, OpenAPI | `http://localhost:8000` |
|
|
||||||
| Dev Console | Internal fixture and contract-testing UI | `http://localhost:5173` |
|
|
||||||
| AIHub | Optional external AI inference dependency | `http://localhost:8080` |
|
|
||||||
|
|
||||||
The service still runs if local AIHub is unavailable: the default config uses `rules_only_review` fallback. A reachable AIHub and a valid tenant VKey are required for live AI scoring.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
Install before starting:
|
|
||||||
|
|
||||||
- Git
|
|
||||||
- Python 3.13 or newer
|
|
||||||
- [uv](https://docs.astral.sh/uv/)
|
|
||||||
- [Bun](https://bun.sh/)
|
|
||||||
- Docker with Docker Compose plugin
|
|
||||||
|
|
||||||
## Start from a fresh clone
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/HungTran24/dotb_ai-platform.git
|
|
||||||
cd dotb_ai-platform
|
|
||||||
cp .env.example .env
|
|
||||||
cp apps/dev-console/.env.example apps/dev-console/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
The copied `.env` files are local only and ignored by Git. Their example values are intentionally for development only.
|
|
||||||
|
|
||||||
### 1. Start PostgreSQL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d postgres
|
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
The included Compose service exposes PostgreSQL on port `5433` to avoid conflicting with other local projects. If port `5433` is occupied, choose another port and update both values before migration:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
POSTGRES_PORT=15432 docker compose up -d postgres
|
|
||||||
# Then set DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:15432/lead_scoring in .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Install backend dependencies and migrate the database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv sync
|
|
||||||
uv run alembic upgrade head
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Start the Lead Scoring API
|
|
||||||
|
|
||||||
Run in a separate terminal from the repository root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run uvicorn lead_scoring.main:app --reload --host 0.0.0.0 --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
Check the API:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
OpenAPI UI: `http://localhost:8000/docs`
|
|
||||||
|
|
||||||
### 4. Start the Dev Console
|
|
||||||
|
|
||||||
Run in a second terminal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/dev-console
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open `http://localhost:5173`.
|
|
||||||
|
|
||||||
## Local configuration
|
|
||||||
|
|
||||||
Root `.env` configures the backend. `apps/dev-console/.env` configures the Vite development server.
|
|
||||||
|
|
||||||
| Variable | Default | Purpose |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `DATABASE_URL` | PostgreSQL at `localhost:5433` | Backend database connection |
|
|
||||||
| `AIHUB_BASE_URL` | `http://localhost:8080` | Optional AIHub endpoint |
|
|
||||||
| `DEV_CONSOLE_API_URL` | `http://localhost:8000` | Dev Console API proxy target |
|
|
||||||
| `DEV_CONSOLE_PORT` | `5173` | Dev Console development port |
|
|
||||||
| `DEV_CONSOLE_PREVIEW_PORT` | `4173` | Dev Console preview port |
|
|
||||||
| `DEV_CONSOLE_SERVICE_TOKEN` | `local-dev-token` | Must match the local backend `SERVICE_CALLERS` entry |
|
|
||||||
| `DEV_CONSOLE_TEST_VKEY` | `local-test-vkey` | Forwarded only to AIHub calls; replace for real AIHub use |
|
|
||||||
|
|
||||||
The local backend template includes an allowlisted `dev-console` caller and baseline scoring config version `1.0.0`, so the Dev Console can open immediately after startup.
|
|
||||||
|
|
||||||
## Stop local services
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
To remove the local database data too:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
Backend:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run pytest
|
|
||||||
uv run ruff check src tests
|
|
||||||
uv run ruff format --check src tests
|
|
||||||
uv run pyright
|
|
||||||
uv run ty check
|
|
||||||
```
|
|
||||||
|
|
||||||
Frontend:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/dev-console
|
|
||||||
bun run check
|
|
||||||
bun run test
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
# A generic, single database configuration.
|
|
||||||
|
|
||||||
[alembic]
|
|
||||||
# path to migration scripts
|
|
||||||
script_location = %(here)s/alembic
|
|
||||||
|
|
||||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
||||||
# file_template = %%(rev)s_%%(slug)s
|
|
||||||
|
|
||||||
# sys.path path, will be prepended to sys.path if present.
|
|
||||||
prepend_sys_path = %(here)s
|
|
||||||
path_separator = os
|
|
||||||
|
|
||||||
# timezone to use when rendering the date within the migration file
|
|
||||||
# as well as the filename.
|
|
||||||
# string value is passed to dateutil.tz.gettz()
|
|
||||||
# leave blank for localtime
|
|
||||||
# timezone =
|
|
||||||
|
|
||||||
# max length of characters to apply to the
|
|
||||||
# "slug" field
|
|
||||||
# truncate_slug_length = 40
|
|
||||||
|
|
||||||
# set to 'true' to run the environment during
|
|
||||||
# the 'revision' command, regardless of autogenerate
|
|
||||||
# revision_environment = false
|
|
||||||
|
|
||||||
# set to 'true' to allow .pyc and .pyo files without
|
|
||||||
# a source .py file to be detected as revisions in the
|
|
||||||
# versions/ directory
|
|
||||||
# sourceless = false
|
|
||||||
|
|
||||||
# version location specification; This defaults
|
|
||||||
# to alembic/versions. When using multiple version
|
|
||||||
# directories, initial revisions must be specified with --version-path.
|
|
||||||
# The path separator used here should be the separator specified by "version_path_separator"
|
|
||||||
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
|
||||||
|
|
||||||
# version path separator
|
|
||||||
version_path_separator = os
|
|
||||||
|
|
||||||
# the output encoding used when revision files
|
|
||||||
# are written from script.py.mako
|
|
||||||
# output_encoding = utf-8
|
|
||||||
|
|
||||||
# Logging configuration
|
|
||||||
[loggers]
|
|
||||||
keys = root,sqlalchemy,alembic
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = console
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = WARN
|
|
||||||
handlers = console
|
|
||||||
qualname =
|
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
|
||||||
level = WARN
|
|
||||||
handlers =
|
|
||||||
qualname = sqlalchemy.engine
|
|
||||||
|
|
||||||
[logger_alembic]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = alembic
|
|
||||||
|
|
||||||
[handler_console]
|
|
||||||
class = StreamHandler
|
|
||||||
args = (sys.stderr,)
|
|
||||||
level = NOTSET
|
|
||||||
formatter = generic
|
|
||||||
|
|
||||||
[formatter_generic]
|
|
||||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
||||||
datefmt = %H:%M:%S
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from logging.config import fileConfig
|
|
||||||
|
|
||||||
from alembic import context
|
|
||||||
from sqlalchemy import pool
|
|
||||||
from sqlalchemy.engine import Connection
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
|
||||||
|
|
||||||
from lead_scoring.config import Settings
|
|
||||||
from lead_scoring.db import Base
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
|
||||||
# access to the values within the .ini file in use.
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
|
||||||
if config.config_file_name is not None:
|
|
||||||
fileConfig(config.config_file_name)
|
|
||||||
|
|
||||||
# add your model's MetaData object here
|
|
||||||
# for 'autogenerate' support
|
|
||||||
target_metadata = Base.metadata
|
|
||||||
|
|
||||||
|
|
||||||
def get_database_url() -> str:
|
|
||||||
attr_url = config.attributes.get("sqlalchemy_url")
|
|
||||||
if attr_url:
|
|
||||||
return str(attr_url)
|
|
||||||
opt_url = config.get_main_option("sqlalchemy.url")
|
|
||||||
if opt_url:
|
|
||||||
return opt_url
|
|
||||||
return Settings().database_url
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
|
||||||
"""Run migrations in 'offline' mode."""
|
|
||||||
url = get_database_url()
|
|
||||||
context.configure(
|
|
||||||
url=url,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
literal_binds=True,
|
|
||||||
dialect_opts={"paramstyle": "named"},
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
def do_run_migrations(connection: Connection) -> None:
|
|
||||||
context.configure(connection=connection, target_metadata=target_metadata)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
async def run_async_migrations() -> None:
|
|
||||||
"""Run migrations in async engine scenario."""
|
|
||||||
url = get_database_url()
|
|
||||||
connectable = create_async_engine(
|
|
||||||
url,
|
|
||||||
poolclass=pool.NullPool,
|
|
||||||
)
|
|
||||||
|
|
||||||
async with connectable.connect() as connection:
|
|
||||||
await connection.run_sync(do_run_migrations)
|
|
||||||
|
|
||||||
await connectable.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
|
||||||
"""Run migrations in 'online' mode."""
|
|
||||||
connectable = config.attributes.get("connection", None)
|
|
||||||
|
|
||||||
if connectable is not None:
|
|
||||||
do_run_migrations(connectable)
|
|
||||||
else:
|
|
||||||
asyncio.run(run_async_migrations())
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
|
||||||
else:
|
|
||||||
run_migrations_online()
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
"""${message}
|
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
|
||||||
Revises: ${down_revision | comma,n}
|
|
||||||
Create Date: ${create_date}
|
|
||||||
|
|
||||||
"""
|
|
||||||
from collections.abc import Sequence
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
${imports if imports else ""}
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = ${repr(up_revision)}
|
|
||||||
down_revision: str | None = ${repr(down_revision)}
|
|
||||||
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
|
|
||||||
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
${upgrades if upgrades else "pass"}
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
${downgrades if downgrades else "pass"}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
"""Initial baseline migration.
|
|
||||||
|
|
||||||
Revision ID: 0001_baseline
|
|
||||||
Revises:
|
|
||||||
Create Date: 2026-08-27
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0001_baseline"
|
|
||||||
down_revision: str | None = None
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
"""Create scoring_runs table.
|
|
||||||
|
|
||||||
Revision ID: 0002_create_scoring_runs
|
|
||||||
Revises: 0001_baseline
|
|
||||||
Create Date: 2026-08-27
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0002_create_scoring_runs"
|
|
||||||
down_revision: str | None = "0001_baseline"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"scoring_runs",
|
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("tenant_id", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("subject_ref", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("scoring_config_version", sa.String(length=64), nullable=False),
|
|
||||||
sa.Column("idempotency_key", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("payload_hash", sa.String(length=64), nullable=False),
|
|
||||||
sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"),
|
|
||||||
sa.Column("snapshot_data", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint(
|
|
||||||
"tenant_id",
|
|
||||||
"idempotency_key",
|
|
||||||
name="uq_scoring_runs_tenant_idempotency",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_table("scoring_runs")
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
"""Add rule-only execution state and transactional outbox.
|
|
||||||
|
|
||||||
Revision ID: 0003_rule_only_execution
|
|
||||||
Revises: 0002_create_scoring_runs
|
|
||||||
Create Date: 2026-08-27
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
revision: str = "0003_rule_only_execution"
|
|
||||||
down_revision: str | None = "0002_create_scoring_runs"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column("scoring_runs", sa.Column("effective_config", postgresql.JSONB(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("confidence_threshold", sa.Float(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("rule_score", sa.Integer(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("confidence", sa.Float(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("error_code", sa.String(length=64), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("retryable", sa.Boolean(), nullable=True))
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs", sa.Column("started_at", sa.DateTime(timezone=True), nullable=True)
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs", sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True)
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs", sa.Column("lease_expires_at", sa.DateTime(timezone=True), nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("event_type", sa.String(length=128), nullable=False),
|
|
||||||
sa.Column("event_version", sa.String(length=32), nullable=False),
|
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("payload", postgresql.JSONB(), nullable=False),
|
|
||||||
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("run_id", "event_type", name="uq_outbox_run_event_type"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_table("outbox_events")
|
|
||||||
op.drop_column("scoring_runs", "lease_expires_at")
|
|
||||||
op.drop_column("scoring_runs", "completed_at")
|
|
||||||
op.drop_column("scoring_runs", "started_at")
|
|
||||||
op.drop_column("scoring_runs", "retryable")
|
|
||||||
op.drop_column("scoring_runs", "error_code")
|
|
||||||
op.drop_column("scoring_runs", "confidence")
|
|
||||||
op.drop_column("scoring_runs", "rule_score")
|
|
||||||
op.drop_column("scoring_runs", "confidence_threshold")
|
|
||||||
op.drop_column("scoring_runs", "effective_config")
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
"""Add medium AI pipeline scoring fields and audit metadata.
|
|
||||||
|
|
||||||
Revision ID: 0004_medium_ai_pipeline
|
|
||||||
Revises: 0003_rule_only_execution
|
|
||||||
Create Date: 2026-08-27
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
revision: str = "0004_medium_ai_pipeline"
|
|
||||||
down_revision: str | None = "0003_rule_only_execution"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column("scoring_runs", sa.Column("ai_score", sa.Integer(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("final_score", sa.Integer(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("scoring_mode", sa.String(length=32), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("degraded", sa.Boolean(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("ai_metadata", postgresql.JSONB(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("scoring_runs", "ai_metadata")
|
|
||||||
op.drop_column("scoring_runs", "degraded")
|
|
||||||
op.drop_column("scoring_runs", "scoring_mode")
|
|
||||||
op.drop_column("scoring_runs", "final_score")
|
|
||||||
op.drop_column("scoring_runs", "ai_score")
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
"""Add review API fields and review audits table.
|
|
||||||
|
|
||||||
Revision ID: 0005_review_api_and_audit
|
|
||||||
Revises: 0004_medium_ai_pipeline
|
|
||||||
Create Date: 2026-08-28
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision: str = "0005_review_api_and_audit"
|
|
||||||
down_revision: str | None = "0004_medium_ai_pipeline"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs",
|
|
||||||
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
|
|
||||||
)
|
|
||||||
op.add_column("scoring_runs", sa.Column("effective_score", sa.Integer(), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("review_decision", sa.String(length=32), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("reviewed_by", sa.String(length=255), nullable=True))
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs", sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True)
|
|
||||||
)
|
|
||||||
op.add_column("scoring_runs", sa.Column("review_note", sa.String(length=2000), nullable=True))
|
|
||||||
|
|
||||||
op.drop_constraint("uq_outbox_run_event_type", "outbox_events", type_="unique")
|
|
||||||
op.create_index(
|
|
||||||
"uq_outbox_terminal_run",
|
|
||||||
"outbox_events",
|
|
||||||
["run_id", "event_type"],
|
|
||||||
unique=True,
|
|
||||||
postgresql_where=sa.text("event_type = 'lead_scoring.run.terminal'"),
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"review_audits",
|
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("tenant_id", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("decision", sa.String(length=32), nullable=False),
|
|
||||||
sa.Column("actor_id", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("actor_role", sa.String(length=32), nullable=False),
|
|
||||||
sa.Column("idempotency_key", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("payload_hash", sa.String(length=64), nullable=False),
|
|
||||||
sa.Column("previous_version", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("new_version", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("calculated_score", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("effective_score", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("note", sa.String(length=2000), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint(
|
|
||||||
"tenant_id",
|
|
||||||
"idempotency_key",
|
|
||||||
name="uq_review_audits_tenant_idempotency",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_table("review_audits")
|
|
||||||
op.drop_index(
|
|
||||||
"uq_outbox_terminal_run",
|
|
||||||
"outbox_events",
|
|
||||||
postgresql_where=sa.text("event_type = 'lead_scoring.run.terminal'"),
|
|
||||||
)
|
|
||||||
# Clean up non-terminal review outbox events before restoring pre-0005 unique constraint
|
|
||||||
op.execute(sa.text("DELETE FROM outbox_events WHERE event_type = 'lead_scoring.run.reviewed'"))
|
|
||||||
op.create_unique_constraint(
|
|
||||||
"uq_outbox_run_event_type", "outbox_events", ["run_id", "event_type"]
|
|
||||||
)
|
|
||||||
op.drop_column("scoring_runs", "review_note")
|
|
||||||
op.drop_column("scoring_runs", "reviewed_at")
|
|
||||||
op.drop_column("scoring_runs", "reviewed_by")
|
|
||||||
op.drop_column("scoring_runs", "review_decision")
|
|
||||||
op.drop_column("scoring_runs", "effective_score")
|
|
||||||
op.drop_column("scoring_runs", "version")
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
"""Add parent_run_id lineage and child_run_id audit for re-evaluate.
|
|
||||||
|
|
||||||
Revision ID: 0006_reevaluate_child_lineage
|
|
||||||
Revises: 0005_review_api_and_audit
|
|
||||||
Create Date: 2026-08-28
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision: str = "0006_reevaluate_child_lineage"
|
|
||||||
down_revision: str | None = "0005_review_api_and_audit"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs",
|
|
||||||
sa.Column(
|
|
||||||
"parent_run_id",
|
|
||||||
sa.String(length=36),
|
|
||||||
sa.ForeignKey("scoring_runs.id"),
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_scoring_runs_parent_run_id",
|
|
||||||
"scoring_runs",
|
|
||||||
["parent_run_id"],
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"review_audits",
|
|
||||||
sa.Column("child_run_id", sa.String(length=36), nullable=True),
|
|
||||||
)
|
|
||||||
op.alter_column(
|
|
||||||
"review_audits",
|
|
||||||
"effective_score",
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.execute(
|
|
||||||
sa.text("UPDATE review_audits SET effective_score = 0 WHERE effective_score IS NULL")
|
|
||||||
)
|
|
||||||
op.alter_column(
|
|
||||||
"review_audits",
|
|
||||||
"effective_score",
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
op.drop_column("review_audits", "child_run_id")
|
|
||||||
op.drop_index("ix_scoring_runs_parent_run_id", table_name="scoring_runs")
|
|
||||||
op.drop_column("scoring_runs", "parent_run_id")
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
"""Add outbox delivery state, retry and lease tracking.
|
|
||||||
|
|
||||||
Revision ID: 0007_outbox_delivery_state
|
|
||||||
Revises: 0006_reevaluate_child_lineage
|
|
||||||
Create Date: 2026-08-28
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision: str = "0007_outbox_delivery_state"
|
|
||||||
down_revision: str | None = "0006_reevaluate_child_lineage"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column(
|
|
||||||
"delivery_status",
|
|
||||||
sa.String(length=32),
|
|
||||||
nullable=False,
|
|
||||||
server_default="pending",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column(
|
|
||||||
"attempt_count",
|
|
||||||
sa.Integer(),
|
|
||||||
nullable=False,
|
|
||||||
server_default="0",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("next_attempt_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("last_attempt_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("delivered_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("lease_expires_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("claim_token", sa.String(length=36), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("last_error_code", sa.String(length=64), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("last_error_detail", sa.String(length=2000), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"outbox_events",
|
|
||||||
sa.Column("last_http_status", sa.Integer(), nullable=True),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_outbox_events_delivery_claim",
|
|
||||||
"outbox_events",
|
|
||||||
["delivery_status", "next_attempt_at"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_index("ix_outbox_events_delivery_claim", table_name="outbox_events")
|
|
||||||
op.drop_column("outbox_events", "last_http_status")
|
|
||||||
op.drop_column("outbox_events", "last_error_detail")
|
|
||||||
op.drop_column("outbox_events", "last_error_code")
|
|
||||||
op.drop_column("outbox_events", "lease_expires_at")
|
|
||||||
op.drop_column("outbox_events", "claim_token")
|
|
||||||
op.drop_column("outbox_events", "delivered_at")
|
|
||||||
op.drop_column("outbox_events", "last_attempt_at")
|
|
||||||
op.drop_column("outbox_events", "next_attempt_at")
|
|
||||||
op.drop_column("outbox_events", "attempt_count")
|
|
||||||
op.drop_column("outbox_events", "delivery_status")
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
"""Add retention and redaction state to scoring_runs and review_audits.
|
|
||||||
|
|
||||||
Revision ID: 0008_retention_redaction_state
|
|
||||||
Revises: 0007_outbox_delivery_state
|
|
||||||
Create Date: 2026-08-28
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision: str = "0008_retention_redaction_state"
|
|
||||||
down_revision: str | None = "0007_outbox_delivery_state"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs",
|
|
||||||
sa.Column("redacted_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_scoring_runs_retention",
|
|
||||||
"scoring_runs",
|
|
||||||
["created_at", "redacted_at"],
|
|
||||||
)
|
|
||||||
|
|
||||||
op.add_column(
|
|
||||||
"review_audits",
|
|
||||||
sa.Column("redacted_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"ix_review_audits_retention",
|
|
||||||
"review_audits",
|
|
||||||
["created_at", "redacted_at"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_index("ix_review_audits_retention", table_name="review_audits")
|
|
||||||
op.drop_column("review_audits", "redacted_at")
|
|
||||||
|
|
||||||
op.drop_index("ix_scoring_runs_retention", table_name="scoring_runs")
|
|
||||||
op.drop_column("scoring_runs", "redacted_at")
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
"""Add execution_trace to scoring_runs.
|
|
||||||
|
|
||||||
Revision ID: 0009_execution_trace
|
|
||||||
Revises: 0008_retention_redaction_state
|
|
||||||
Create Date: 2026-09-04
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
revision: str = "0009_execution_trace"
|
|
||||||
down_revision: str | None = "0008_retention_redaction_state"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"scoring_runs",
|
|
||||||
sa.Column("execution_trace", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("scoring_runs", "execution_trace")
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
"""Persist originating DotB headers for AIHub forwarding.
|
|
||||||
|
|
||||||
Revision ID: 0010_dotb_headers
|
|
||||||
Revises: 0009_execution_trace
|
|
||||||
Create Date: 2026-09-07
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision: str = "0010_dotb_headers"
|
|
||||||
down_revision: str | None = "0009_execution_trace"
|
|
||||||
branch_labels: str | None = None
|
|
||||||
depends_on: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column("scoring_runs", sa.Column("dotb_app", sa.String(length=64), nullable=True))
|
|
||||||
op.add_column("scoring_runs", sa.Column("dotb_feature", sa.String(length=64), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("scoring_runs", "dotb_feature")
|
|
||||||
op.drop_column("scoring_runs", "dotb_app")
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
DEV_CONSOLE_API_URL=http://localhost:8000
|
|
||||||
DEV_CONSOLE_PORT=5173
|
|
||||||
DEV_CONSOLE_PREVIEW_PORT=4173
|
|
||||||
# Must match SERVICE_CALLERS in the root .env for the local setup.
|
|
||||||
DEV_CONSOLE_SERVICE_TOKEN=local-dev-token
|
|
||||||
# Used only to call a local AIHub. Any non-empty value exercises rules-only fallback when AIHub is absent.
|
|
||||||
DEV_CONSOLE_TEST_VKEY=local-test-vkey
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
{
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
|
||||||
"": {
|
|
||||||
"name": "dev-console",
|
|
||||||
"dependencies": {
|
|
||||||
"openapi-fetch": "^0.13.4",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
||||||
"openapi-typescript": "^7.4.4",
|
|
||||||
"svelte": "^5.2.7",
|
|
||||||
"svelte-check": "^4.1.1",
|
|
||||||
"typescript": "^5.7.2",
|
|
||||||
"vite": "^5.4.11",
|
|
||||||
"vitest": "^2.1.8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages": {
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
|
||||||
|
|
||||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
|
||||||
|
|
||||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
|
||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
|
||||||
|
|
||||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
|
||||||
|
|
||||||
"@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="],
|
|
||||||
|
|
||||||
"@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="],
|
|
||||||
|
|
||||||
"@redocly/openapi-core": ["@redocly/openapi-core@1.34.19", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.3.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-o/0VgsBXgwcY1lyeqcVtSGdTQAPnVggo0fbFVPlxl5XVDKUcVH0OLRqt3CbkwByT5FU305E0iE0O7MzThjDblw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.0", "", { "os": "android", "cpu": "arm" }, "sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.0", "", { "os": "none", "cpu": "arm64" }, "sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ=="],
|
|
||||||
|
|
||||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
|
|
||||||
|
|
||||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
|
||||||
|
|
||||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@4.0.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.12", "vitefu": "^1.0.3" }, "peerDependencies": { "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA=="],
|
|
||||||
|
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@3.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ=="],
|
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
|
||||||
|
|
||||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="],
|
|
||||||
|
|
||||||
"@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="],
|
|
||||||
|
|
||||||
"@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="],
|
|
||||||
|
|
||||||
"@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="],
|
|
||||||
|
|
||||||
"@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="],
|
|
||||||
|
|
||||||
"@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="],
|
|
||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="],
|
|
||||||
|
|
||||||
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
|
||||||
|
|
||||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
|
||||||
|
|
||||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
|
||||||
|
|
||||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
|
||||||
|
|
||||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
|
||||||
|
|
||||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
|
||||||
|
|
||||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
|
||||||
|
|
||||||
"change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
|
|
||||||
|
|
||||||
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
|
||||||
|
|
||||||
"colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
|
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
||||||
|
|
||||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
|
||||||
|
|
||||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
|
||||||
|
|
||||||
"devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="],
|
|
||||||
|
|
||||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
|
||||||
|
|
||||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
|
||||||
|
|
||||||
"esrap": ["esrap@2.3.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A=="],
|
|
||||||
|
|
||||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
|
||||||
|
|
||||||
"expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
|
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
|
||||||
|
|
||||||
"index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
|
|
||||||
|
|
||||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
|
||||||
|
|
||||||
"js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
|
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
|
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
|
||||||
|
|
||||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
|
||||||
|
|
||||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
|
||||||
|
|
||||||
"minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
|
||||||
|
|
||||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
|
||||||
|
|
||||||
"openapi-fetch": ["openapi-fetch@0.13.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ=="],
|
|
||||||
|
|
||||||
"openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="],
|
|
||||||
|
|
||||||
"openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.15", "", {}, "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw=="],
|
|
||||||
|
|
||||||
"parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
|
|
||||||
|
|
||||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
|
||||||
|
|
||||||
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
|
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
|
||||||
|
|
||||||
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
|
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
|
||||||
|
|
||||||
"rollup": ["rollup@4.63.0", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.0", "@rollup/rollup-android-arm64": "4.63.0", "@rollup/rollup-darwin-arm64": "4.63.0", "@rollup/rollup-darwin-x64": "4.63.0", "@rollup/rollup-freebsd-arm64": "4.63.0", "@rollup/rollup-freebsd-x64": "4.63.0", "@rollup/rollup-linux-arm-gnueabihf": "4.63.0", "@rollup/rollup-linux-arm-musleabihf": "4.63.0", "@rollup/rollup-linux-arm64-gnu": "4.63.0", "@rollup/rollup-linux-arm64-musl": "4.63.0", "@rollup/rollup-linux-loong64-gnu": "4.63.0", "@rollup/rollup-linux-loong64-musl": "4.63.0", "@rollup/rollup-linux-ppc64-gnu": "4.63.0", "@rollup/rollup-linux-ppc64-musl": "4.63.0", "@rollup/rollup-linux-riscv64-gnu": "4.63.0", "@rollup/rollup-linux-riscv64-musl": "4.63.0", "@rollup/rollup-linux-s390x-gnu": "4.63.0", "@rollup/rollup-linux-x64-gnu": "4.63.0", "@rollup/rollup-linux-x64-musl": "4.63.0", "@rollup/rollup-openbsd-x64": "4.63.0", "@rollup/rollup-openharmony-arm64": "4.63.0", "@rollup/rollup-win32-arm64-msvc": "4.63.0", "@rollup/rollup-win32-ia32-msvc": "4.63.0", "@rollup/rollup-win32-x64-gnu": "4.63.0", "@rollup/rollup-win32-x64-msvc": "4.63.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ=="],
|
|
||||||
|
|
||||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
|
||||||
|
|
||||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
|
||||||
|
|
||||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
|
||||||
|
|
||||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
|
||||||
|
|
||||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
|
||||||
|
|
||||||
"svelte": ["svelte@5.56.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g=="],
|
|
||||||
|
|
||||||
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
|
|
||||||
|
|
||||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
|
||||||
|
|
||||||
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
|
|
||||||
|
|
||||||
"tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="],
|
|
||||||
|
|
||||||
"tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="],
|
|
||||||
|
|
||||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
||||||
|
|
||||||
"uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
|
|
||||||
|
|
||||||
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
|
||||||
|
|
||||||
"vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="],
|
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
|
||||||
|
|
||||||
"vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="],
|
|
||||||
|
|
||||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
|
||||||
|
|
||||||
"yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
|
|
||||||
|
|
||||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
|
||||||
|
|
||||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="vi">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>DotB AI Platform</title>
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "dev-console",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
||||||
"test": "vitest run",
|
|
||||||
"generate-api": "openapi-typescript ../../openapi.json -o ./src/lib/api/schema.d.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"openapi-fetch": "^0.13.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
||||||
"openapi-typescript": "^7.4.4",
|
|
||||||
"svelte": "^5.2.7",
|
|
||||||
"svelte-check": "^4.1.1",
|
|
||||||
"typescript": "^5.7.2",
|
|
||||||
"vite": "^5.4.11",
|
|
||||||
"vitest": "^2.1.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 1884.0267 967.99597"
|
|
||||||
height="967.99597"
|
|
||||||
width="1884.0267"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg2"
|
|
||||||
version="1.1"><metadata
|
|
||||||
id="metadata8"><rdf:RDF><cc:Work
|
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
|
||||||
id="defs6"><linearGradient
|
|
||||||
id="linearGradient28"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(305.44366,0,0,305.44366,581.11853,367.54431)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop24"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop26"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient48"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(347.46957,0,0,347.46957,565.36786,367.3493)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop44"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop46"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient68"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(347.45862,0,0,347.45862,563.54041,299.44431)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop64"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop66"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient88"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(347.46759,0,0,-347.46759,558.06158,366.5343)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop84"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop86"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient108"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(105.03998,0,0,-105.03998,989.06964,267.11432)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop104"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#ae3ba4" /><stop
|
|
||||||
id="stop106"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#ed0a04" /></linearGradient></defs><g
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,967.996)"
|
|
||||||
id="g10"><g
|
|
||||||
id="g12"><g
|
|
||||||
id="g14"><g
|
|
||||||
id="g20"><g
|
|
||||||
id="g22"><path
|
|
||||||
id="path30"
|
|
||||||
style="fill:url(#linearGradient28);stroke:none"
|
|
||||||
d="M 748.82,594.175 V 304.434 c -0.08,-1.51 -0.13,-3.01 -0.16,-4.52 v 0 c 0.039,-1.07 0.099,-2.129 0.16,-3.199 v 0 -1.18 h 0.06 c 2.37,-86.011 72.73,-154.531 158.78,-154.62 v 0 c 44.259,0 84.289,18.08 113.12,47.269 v 0 c 2.57,2.59 5.04,5.27 7.42,8.04 v 0 c -29.401,5.24 -52.66,28.17 -58.41,57.38 v 0 c -1.961,-2.529 -4.081,-4.93 -6.34,-7.189 v 0 c -2.75,-2.751 -5.71,-5.291 -8.861,-7.58 v 0 c -12.5,-9.16 -27.909,-14.561 -44.59,-14.561 v 0 c -20.879,0 -39.769,8.46 -53.45,22.141 v 0 c -13.679,13.68 -22.139,32.58 -22.139,53.45 v 0 c 0,1.059 0.02,2.109 0.07,3.159 v 0 137.91 c -0.101,-0.05 -0.201,-0.1 -0.3,-0.15 v 0 17.52 h 0.3 v 0.48 h 88.71 v 70.681 h -89.01 v 64.71 z" /></g></g></g></g><g
|
|
||||||
id="g32"><g
|
|
||||||
id="g34"><g
|
|
||||||
id="g40"><g
|
|
||||||
id="g42"><path
|
|
||||||
id="path50"
|
|
||||||
style="fill:url(#linearGradient48);stroke:none"
|
|
||||||
d="m 286.03,594.255 v -153.71 c -22.61,11.739 -47.7,17.879 -73.18,17.9 v 0 c -87.81,0 -159,-71.181 -159,-159.001 v 0 c 0,-87.81 71.19,-158.999 159,-158.999 v 0 c 87.809,0 158.999,71.189 158.999,158.999 v 0 c -0.06,3.06 -0.219,6.111 -0.46,9.15 v 0 285.661 z M 136.55,298.534 c 0,20.88 8.46,39.77 22.14,53.45 v 0 c 13.67,13.681 32.57,22.141 53.44,22.141 v 0 h 0.01 c 35.73,-0.041 66.559,-25.111 73.89,-60.081 v 0 -23.719 h 1.219 C 283.06,251.995 250.69,222.964 212.14,222.944 v 0 c -41.75,0 -75.59,33.84 -75.59,75.59" /></g></g></g></g><g
|
|
||||||
id="g52"><g
|
|
||||||
id="g54"><g
|
|
||||||
id="g60"><g
|
|
||||||
id="g62"><path
|
|
||||||
id="path70"
|
|
||||||
style="fill:url(#linearGradient68);stroke:none"
|
|
||||||
d="m 402.349,299.444 c 0,-87.81 71.181,-158.999 159.001,-158.999 v 0 c 87.809,0 158.99,71.189 158.99,158.999 v 0 c 0,87.82 -71.181,159.001 -158.99,159.001 v 0 c -87.82,0 -159.001,-71.181 -159.001,-159.001 m 83.871,0.16 c 0,20.87 8.46,39.771 22.14,53.45 v 0 c 13.68,13.68 32.58,22.141 53.45,22.141 v 0 c 41.75,0 75.59,-33.841 75.59,-75.591 v 0 c 0,-41.75 -33.84,-75.59 -75.59,-75.59 v 0 c -41.75,0 -75.59,33.84 -75.59,75.59" /></g></g></g></g><g
|
|
||||||
id="g72"><g
|
|
||||||
id="g74"><g
|
|
||||||
id="g80"><g
|
|
||||||
id="g82"><path
|
|
||||||
id="path90"
|
|
||||||
style="fill:url(#linearGradient88);stroke:none"
|
|
||||||
d="m 1041.81,593.324 v -252.99 c 7.099,-0.07 13.949,-1.16 20.42,-3.139 v 0 -0.011 c 29.77,-9.059 51.42,-36.739 51.42,-69.47 v 0 c 0,-20.75 -8.71,-39.47 -22.67,-52.709 v 0 c -5.05,-4.791 -10.8,-8.871 -17.08,-12.06 v 0 c 4.21,-5.571 8.78,-10.861 13.68,-15.821 v 0 c 28.839,-29.25 68.919,-47.379 113.23,-47.379 v 0 c 87.819,-0.01 159,71.179 159,158.988 v 0 c 0,43.911 -17.79,83.661 -46.57,112.431 v 0 c -28.77,28.77 -68.52,46.571 -112.43,46.571 v 0 c -12.83,-0.011 -25.55,-1.581 -37.91,-4.621 v 0 c -12.361,-3.06 -24.36,-7.59 -35.72,-13.54 v 0 153.75 z m 82.7,-294.129 c 0,41.75 33.84,75.589 75.59,75.589 v 0 c 41.739,0 75.59,-33.839 75.59,-75.589 v 0 c 0,-41.75 -33.851,-75.591 -75.59,-75.591 v 0 c -41.75,0 -75.59,33.841 -75.59,75.591" /></g></g></g></g><g
|
|
||||||
id="g92"><g
|
|
||||||
id="g94"><g
|
|
||||||
id="g100"><g
|
|
||||||
id="g102"><path
|
|
||||||
id="path110"
|
|
||||||
style="fill:url(#linearGradient108);stroke:none"
|
|
||||||
d="m 1041.33,319.634 c -0.71,0 -1.421,-0.02 -2.121,-0.059 v 0 c -15.789,-0.701 -29.759,-8.37 -38.909,-20.011 v 0 c -7.041,-8.94 -11.231,-20.21 -11.231,-32.449 v 0 c 0,-15.75 6.93,-29.871 17.911,-39.5 v 0 h 0.01 c 0.991,-0.882 2.01,-1.711 3.071,-2.501 v 0 c 0.04,-0.029 0.069,-0.06 0.109,-0.09 v 0 c 8.761,-6.55 19.641,-10.43 31.42,-10.43 v 0 h 0.261 c 7.57,0.04 14.77,1.68 21.26,4.601 v 0 c 18.27,8.21 31,26.579 31,47.92 v 0 c 0,2.799 -0.22,5.54 -0.64,8.219 v 0 h -0.011 c -2.6,16.63 -13.029,30.68 -27.389,38.25 v 0 c -6.82,3.611 -14.531,5.75 -22.71,6.01 v 0 c -0.59,0.03 -1.18,0.04 -1.771,0.04 v 0 h -0.259 z" /></g></g></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 6.9 KiB |
@ -1,124 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 1884.0267 967.99597"
|
|
||||||
height="967.99597"
|
|
||||||
width="1884.0267"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg2"
|
|
||||||
version="1.1"><metadata
|
|
||||||
id="metadata8"><rdf:RDF><cc:Work
|
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
|
||||||
id="defs6"><linearGradient
|
|
||||||
id="linearGradient28"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(305.44366,0,0,305.44366,581.11853,367.54431)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop24"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop26"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient48"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(347.46957,0,0,347.46957,565.36786,367.3493)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop44"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop46"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient68"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(347.45862,0,0,347.45862,563.54041,299.44431)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop64"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop66"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient88"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(347.46759,0,0,-347.46759,558.06158,366.5343)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop84"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop86"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient108"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(105.03998,0,0,-105.03998,989.06964,267.11432)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop104"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#ae3ba4" /><stop
|
|
||||||
id="stop106"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#ed0a04" /></linearGradient></defs><g
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,967.996)"
|
|
||||||
id="g10"><g
|
|
||||||
id="g12"><g
|
|
||||||
id="g14"><g
|
|
||||||
id="g20"><g
|
|
||||||
id="g22"><path
|
|
||||||
id="path30"
|
|
||||||
style="fill:url(#linearGradient28);stroke:none"
|
|
||||||
d="M 748.82,594.175 V 304.434 c -0.08,-1.51 -0.13,-3.01 -0.16,-4.52 v 0 c 0.039,-1.07 0.099,-2.129 0.16,-3.199 v 0 -1.18 h 0.06 c 2.37,-86.011 72.73,-154.531 158.78,-154.62 v 0 c 44.259,0 84.289,18.08 113.12,47.269 v 0 c 2.57,2.59 5.04,5.27 7.42,8.04 v 0 c -29.401,5.24 -52.66,28.17 -58.41,57.38 v 0 c -1.961,-2.529 -4.081,-4.93 -6.34,-7.189 v 0 c -2.75,-2.751 -5.71,-5.291 -8.861,-7.58 v 0 c -12.5,-9.16 -27.909,-14.561 -44.59,-14.561 v 0 c -20.879,0 -39.769,8.46 -53.45,22.141 v 0 c -13.679,13.68 -22.139,32.58 -22.139,53.45 v 0 c 0,1.059 0.02,2.109 0.07,3.159 v 0 137.91 c -0.101,-0.05 -0.201,-0.1 -0.3,-0.15 v 0 17.52 h 0.3 v 0.48 h 88.71 v 70.681 h -89.01 v 64.71 z" /></g></g></g></g><g
|
|
||||||
id="g32"><g
|
|
||||||
id="g34"><g
|
|
||||||
id="g40"><g
|
|
||||||
id="g42"><path
|
|
||||||
id="path50"
|
|
||||||
style="fill:url(#linearGradient48);stroke:none"
|
|
||||||
d="m 286.03,594.255 v -153.71 c -22.61,11.739 -47.7,17.879 -73.18,17.9 v 0 c -87.81,0 -159,-71.181 -159,-159.001 v 0 c 0,-87.81 71.19,-158.999 159,-158.999 v 0 c 87.809,0 158.999,71.189 158.999,158.999 v 0 c -0.06,3.06 -0.219,6.111 -0.46,9.15 v 0 285.661 z M 136.55,298.534 c 0,20.88 8.46,39.77 22.14,53.45 v 0 c 13.67,13.681 32.57,22.141 53.44,22.141 v 0 h 0.01 c 35.73,-0.041 66.559,-25.111 73.89,-60.081 v 0 -23.719 h 1.219 C 283.06,251.995 250.69,222.964 212.14,222.944 v 0 c -41.75,0 -75.59,33.84 -75.59,75.59" /></g></g></g></g><g
|
|
||||||
id="g52"><g
|
|
||||||
id="g54"><g
|
|
||||||
id="g60"><g
|
|
||||||
id="g62"><path
|
|
||||||
id="path70"
|
|
||||||
style="fill:url(#linearGradient68);stroke:none"
|
|
||||||
d="m 402.349,299.444 c 0,-87.81 71.181,-158.999 159.001,-158.999 v 0 c 87.809,0 158.99,71.189 158.99,158.999 v 0 c 0,87.82 -71.181,159.001 -158.99,159.001 v 0 c -87.82,0 -159.001,-71.181 -159.001,-159.001 m 83.871,0.16 c 0,20.87 8.46,39.771 22.14,53.45 v 0 c 13.68,13.68 32.58,22.141 53.45,22.141 v 0 c 41.75,0 75.59,-33.841 75.59,-75.591 v 0 c 0,-41.75 -33.84,-75.59 -75.59,-75.59 v 0 c -41.75,0 -75.59,33.84 -75.59,75.59" /></g></g></g></g><g
|
|
||||||
id="g72"><g
|
|
||||||
id="g74"><g
|
|
||||||
id="g80"><g
|
|
||||||
id="g82"><path
|
|
||||||
id="path90"
|
|
||||||
style="fill:url(#linearGradient88);stroke:none"
|
|
||||||
d="m 1041.81,593.324 v -252.99 c 7.099,-0.07 13.949,-1.16 20.42,-3.139 v 0 -0.011 c 29.77,-9.059 51.42,-36.739 51.42,-69.47 v 0 c 0,-20.75 -8.71,-39.47 -22.67,-52.709 v 0 c -5.05,-4.791 -10.8,-8.871 -17.08,-12.06 v 0 c 4.21,-5.571 8.78,-10.861 13.68,-15.821 v 0 c 28.839,-29.25 68.919,-47.379 113.23,-47.379 v 0 c 87.819,-0.01 159,71.179 159,158.988 v 0 c 0,43.911 -17.79,83.661 -46.57,112.431 v 0 c -28.77,28.77 -68.52,46.571 -112.43,46.571 v 0 c -12.83,-0.011 -25.55,-1.581 -37.91,-4.621 v 0 c -12.361,-3.06 -24.36,-7.59 -35.72,-13.54 v 0 153.75 z m 82.7,-294.129 c 0,41.75 33.84,75.589 75.59,75.589 v 0 c 41.739,0 75.59,-33.839 75.59,-75.589 v 0 c 0,-41.75 -33.851,-75.591 -75.59,-75.591 v 0 c -41.75,0 -75.59,33.841 -75.59,75.591" /></g></g></g></g><g
|
|
||||||
id="g92"><g
|
|
||||||
id="g94"><g
|
|
||||||
id="g100"><g
|
|
||||||
id="g102"><path
|
|
||||||
id="path110"
|
|
||||||
style="fill:url(#linearGradient108);stroke:none"
|
|
||||||
d="m 1041.33,319.634 c -0.71,0 -1.421,-0.02 -2.121,-0.059 v 0 c -15.789,-0.701 -29.759,-8.37 -38.909,-20.011 v 0 c -7.041,-8.94 -11.231,-20.21 -11.231,-32.449 v 0 c 0,-15.75 6.93,-29.871 17.911,-39.5 v 0 h 0.01 c 0.991,-0.882 2.01,-1.711 3.071,-2.501 v 0 c 0.04,-0.029 0.069,-0.06 0.109,-0.09 v 0 c 8.761,-6.55 19.641,-10.43 31.42,-10.43 v 0 h 0.261 c 7.57,0.04 14.77,1.68 21.26,4.601 v 0 c 18.27,8.21 31,26.579 31,47.92 v 0 c 0,2.799 -0.22,5.54 -0.64,8.219 v 0 h -0.011 c -2.6,16.63 -13.029,30.68 -27.389,38.25 v 0 c -6.82,3.611 -14.531,5.75 -22.71,6.01 v 0 c -0.59,0.03 -1.18,0.04 -1.771,0.04 v 0 h -0.259 z" /></g></g></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 6.9 KiB |
@ -1,61 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 566.92932 566.92932"
|
|
||||||
height="566.92932"
|
|
||||||
width="566.92932"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg2"
|
|
||||||
version="1.1"><metadata
|
|
||||||
id="metadata8"><rdf:RDF><cc:Work
|
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
|
||||||
id="defs6"><linearGradient
|
|
||||||
id="linearGradient28"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(261.43005,0,0,-261.43005,107.24294,217.74225)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop24"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop26"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient48"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(86.359985,0,0,-86.359985,63.88295,136.00223)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop44"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#ae3ba4" /><stop
|
|
||||||
id="stop46"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#ed0a04" /></linearGradient></defs><g
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,566.92933)"
|
|
||||||
id="g10"><g
|
|
||||||
id="g12"><g
|
|
||||||
id="g14"><g
|
|
||||||
id="g20"><g
|
|
||||||
id="g22"><path
|
|
||||||
id="path30"
|
|
||||||
style="fill:url(#linearGradient28);stroke:none"
|
|
||||||
d="m 107.243,404.187 v -208.48 c 32.89,-0.1 59.53,-26.79 59.53,-59.7 v 0 c 0,-23.269 -13.32,-43.43 -32.75,-53.28 v 0 c 23.88,-31.26 61.55,-51.43 103.93,-51.43 v 0 c 34.67,0 67.92,13.771 92.43,38.28 v 0 c 24.519,24.51 38.289,57.76 38.289,92.429 v 0 c 0,34.672 -13.77,67.922 -38.289,92.431 v 0 c -24.51,24.51 -57.76,38.28 -92.43,38.28 v 0 c -21.08,-0.02 -41.85,-5.14 -60.531,-14.93 v 0 126.4 z m 67.98,-241.81 c 0,34.32 27.82,62.15 62.15,62.15 v 0 c 34.32,0 62.14,-27.83 62.14,-62.15 v 0 c 0,-34.32 -27.82,-62.139 -62.14,-62.139 v 0 c -34.33,0 -62.15,27.819 -62.15,62.139" /></g></g></g></g><g
|
|
||||||
id="g32"><g
|
|
||||||
id="g34"><g
|
|
||||||
id="g40"><g
|
|
||||||
id="g42"><path
|
|
||||||
id="path50"
|
|
||||||
style="fill:url(#linearGradient48);stroke:none"
|
|
||||||
d="m 63.883,136.007 c 0,-23.85 19.34,-43.18 43.18,-43.18 v 0 c 6.31,0 12.3,1.351 17.7,3.78 v 0 c 15.02,6.76 25.48,21.861 25.48,39.4 v 0 c 0,23.36 -18.551,42.38 -41.73,43.15 v 0 c -0.48,0.01 -0.97,0.02 -1.45,0.02 v 0 c -23.84,0 -43.18,-19.33 -43.18,-43.17" /></g></g></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@ -1,61 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 566.92932 566.92932"
|
|
||||||
height="566.92932"
|
|
||||||
width="566.92932"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg2"
|
|
||||||
version="1.1"><metadata
|
|
||||||
id="metadata8"><rdf:RDF><cc:Work
|
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
|
||||||
id="defs6"><linearGradient
|
|
||||||
id="linearGradient28"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(261.43005,0,0,-261.43005,107.24294,217.74225)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop24"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#00c9fd" /><stop
|
|
||||||
id="stop26"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#81ee8e" /></linearGradient><linearGradient
|
|
||||||
id="linearGradient48"
|
|
||||||
spreadMethod="pad"
|
|
||||||
gradientTransform="matrix(86.359985,0,0,-86.359985,63.88295,136.00223)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
y2="0"
|
|
||||||
x2="1"
|
|
||||||
y1="0"
|
|
||||||
x1="0"><stop
|
|
||||||
id="stop44"
|
|
||||||
offset="0"
|
|
||||||
style="stop-opacity:1;stop-color:#ae3ba4" /><stop
|
|
||||||
id="stop46"
|
|
||||||
offset="1"
|
|
||||||
style="stop-opacity:1;stop-color:#ed0a04" /></linearGradient></defs><g
|
|
||||||
transform="matrix(1.3333333,0,0,-1.3333333,0,566.92933)"
|
|
||||||
id="g10"><g
|
|
||||||
id="g12"><g
|
|
||||||
id="g14"><g
|
|
||||||
id="g20"><g
|
|
||||||
id="g22"><path
|
|
||||||
id="path30"
|
|
||||||
style="fill:url(#linearGradient28);stroke:none"
|
|
||||||
d="m 107.243,404.187 v -208.48 c 32.89,-0.1 59.53,-26.79 59.53,-59.7 v 0 c 0,-23.269 -13.32,-43.43 -32.75,-53.28 v 0 c 23.88,-31.26 61.55,-51.43 103.93,-51.43 v 0 c 34.67,0 67.92,13.771 92.43,38.28 v 0 c 24.519,24.51 38.289,57.76 38.289,92.429 v 0 c 0,34.672 -13.77,67.922 -38.289,92.431 v 0 c -24.51,24.51 -57.76,38.28 -92.43,38.28 v 0 c -21.08,-0.02 -41.85,-5.14 -60.531,-14.93 v 0 126.4 z m 67.98,-241.81 c 0,34.32 27.82,62.15 62.15,62.15 v 0 c 34.32,0 62.14,-27.83 62.14,-62.15 v 0 c 0,-34.32 -27.82,-62.139 -62.14,-62.139 v 0 c -34.33,0 -62.15,27.819 -62.15,62.139" /></g></g></g></g><g
|
|
||||||
id="g32"><g
|
|
||||||
id="g34"><g
|
|
||||||
id="g40"><g
|
|
||||||
id="g42"><path
|
|
||||||
id="path50"
|
|
||||||
style="fill:url(#linearGradient48);stroke:none"
|
|
||||||
d="m 63.883,136.007 c 0,-23.85 19.34,-43.18 43.18,-43.18 v 0 c 6.31,0 12.3,1.351 17.7,3.78 v 0 c 15.02,6.76 25.48,21.861 25.48,39.4 v 0 c 0,23.36 -18.551,42.38 -41.73,43.15 v 0 c -0.48,0.01 -0.97,0.02 -1.45,0.02 v 0 c -23.84,0 -43.18,-19.33 -43.18,-43.17" /></g></g></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 108 KiB |
@ -1,436 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import DemoFlow from './lib/components/DemoFlow.svelte';
|
|
||||||
import ChurnPrediction from './lib/components/ChurnPrediction.svelte';
|
|
||||||
import Icon from './lib/components/Icon.svelte';
|
|
||||||
|
|
||||||
let currentNav = $state<'lead-scoring' | 'churn-prediction'>('lead-scoring');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="app-layout">
|
|
||||||
<!-- Left Sidebar -->
|
|
||||||
<aside class="sidebar" aria-label="Menu điều hướng chính">
|
|
||||||
<!-- Brand Header -->
|
|
||||||
<div class="brand-header">
|
|
||||||
<div class="brand-logo-wrap">
|
|
||||||
<img src="/brand-assets/logo_thumbnail.svg" alt="DotB" class="brand-logo" width="28" height="28" />
|
|
||||||
</div>
|
|
||||||
<div class="brand-title-wrap">
|
|
||||||
<span class="brand-name">DotB</span>
|
|
||||||
<span class="brand-product">AI Platform</span>
|
|
||||||
</div>
|
|
||||||
<span class="env-chip">DEV</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Section: AI Services -->
|
|
||||||
<div class="nav-group">
|
|
||||||
<span class="nav-heading">DỊCH VỤ AI</span>
|
|
||||||
<nav class="nav-list">
|
|
||||||
<button
|
|
||||||
class="nav-item"
|
|
||||||
class:active={currentNav === 'lead-scoring'}
|
|
||||||
type="button"
|
|
||||||
onclick={() => (currentNav = 'lead-scoring')}
|
|
||||||
>
|
|
||||||
<div class="nav-item-content">
|
|
||||||
<Icon name="target" size={15} />
|
|
||||||
<span class="nav-label">Lead Scoring</span>
|
|
||||||
</div>
|
|
||||||
<span class="badge-live">Live</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="nav-item"
|
|
||||||
class:active={currentNav === 'churn-prediction'}
|
|
||||||
type="button"
|
|
||||||
onclick={() => (currentNav = 'churn-prediction')}
|
|
||||||
>
|
|
||||||
<div class="nav-item-content">
|
|
||||||
<Icon name="trending-down" size={15} />
|
|
||||||
<span class="nav-label">Churn Prediction</span>
|
|
||||||
</div>
|
|
||||||
<span class="badge-dev">In Dev</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Section: Developer Resources -->
|
|
||||||
<div class="nav-group">
|
|
||||||
<span class="nav-heading">TÀI NGUYÊN KỸ THUẬT</span>
|
|
||||||
<nav class="nav-list">
|
|
||||||
<a class="nav-item nav-link" href="/lead-scoring-api-docs.html" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="nav-item-content">
|
|
||||||
<Icon name="book-open" size={15} />
|
|
||||||
<span class="nav-label">API Reference</span>
|
|
||||||
</div>
|
|
||||||
<span class="nav-ext-icon">↗</span>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sidebar Footer: Status & Tenant -->
|
|
||||||
<div class="sidebar-footer">
|
|
||||||
<div class="footer-status-row">
|
|
||||||
<span class="status-dot"></span>
|
|
||||||
<span class="status-text">AIHub Gateway Online</span>
|
|
||||||
</div>
|
|
||||||
<div class="footer-meta-row">
|
|
||||||
<span class="meta-label">Tenant:</span>
|
|
||||||
<code class="meta-value">omni-account-42</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
|
||||||
<main class="main-wrapper">
|
|
||||||
<!-- Top Bar -->
|
|
||||||
<header class="topbar">
|
|
||||||
<div class="breadcrumb">
|
|
||||||
<span class="breadcrumb-root">AI Platform</span>
|
|
||||||
<span class="breadcrumb-separator">/</span>
|
|
||||||
<span class="breadcrumb-current">
|
|
||||||
{currentNav === 'lead-scoring' ? 'Lead Scoring Engine' : 'Student Churn Prediction'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="topbar-actions">
|
|
||||||
<div class="backend-pill">
|
|
||||||
<span class="pill-dot"></span>
|
|
||||||
<span>Core API: <code>127.0.0.1</code></span>
|
|
||||||
</div>
|
|
||||||
<a class="doc-btn" href="/lead-scoring-api-docs.html" target="_blank" rel="noopener noreferrer">
|
|
||||||
Tài liệu API (Redoc) ↗
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Content Body -->
|
|
||||||
<div class="content-body">
|
|
||||||
{#if currentNav === 'lead-scoring'}
|
|
||||||
<DemoFlow />
|
|
||||||
{:else if currentNav === 'churn-prediction'}
|
|
||||||
<ChurnPrediction />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.app-layout {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: var(--bg-canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border-right: 1px solid var(--border-subtle);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
height: 100vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 18px 20px;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-logo-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-logo {
|
|
||||||
display: block;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-title-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
line-height: 1.15;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
font-size: 0.92rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-primary);
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-product {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.env-chip {
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
background: var(--accent-blue-subtle);
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
font-size: 0.62rem;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
border: 1px solid var(--accent-blue-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav Groups */
|
|
||||||
.nav-group {
|
|
||||||
padding: 16px 14px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-heading {
|
|
||||||
display: block;
|
|
||||||
padding: 0 8px 8px;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 10px;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:hover {
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active {
|
|
||||||
background: var(--accent-blue-subtle);
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
border-color: var(--accent-blue-border);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-live {
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background: #ecfdf5;
|
|
||||||
color: #047857;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid #a7f3d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-dev {
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid #fde68a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-ext-icon {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar Footer */
|
|
||||||
.sidebar-footer {
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-top: 1px solid var(--border-subtle);
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-status-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #10b981;
|
|
||||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-meta-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-label {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-value {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Wrapper */
|
|
||||||
.main-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Topbar */
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 28px;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-root {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-separator {
|
|
||||||
color: var(--border-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-current {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backend-pill {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backend-pill code {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 5px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--accent-blue-subtle);
|
|
||||||
border: 1px solid var(--accent-blue-border);
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-btn:hover {
|
|
||||||
background: var(--bg-surface-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-body {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.sidebar {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.app-layout {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.sidebar {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.sidebar-footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.topbar {
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Canvas & Atmospheric Light Grounding */
|
|
||||||
--bg-canvas: #f8fafc;
|
|
||||||
--bg-canvas-subtle: #f1f5f9;
|
|
||||||
--bg-grid-line: rgba(15, 23, 42, 0.03);
|
|
||||||
|
|
||||||
/* Surface System (Clean White & Subtle Tiers) */
|
|
||||||
--bg-surface: #ffffff;
|
|
||||||
--bg-surface-raised: #f1f5f9;
|
|
||||||
--bg-surface-subtle: #f8fafc;
|
|
||||||
--bg-surface-hover: #e2e8f0;
|
|
||||||
--bg-surface-active: #cbd5e1;
|
|
||||||
--bg-code: #0f172a;
|
|
||||||
|
|
||||||
/* Borders & Dividers */
|
|
||||||
--border-subtle: rgba(15, 23, 42, 0.08);
|
|
||||||
--border-medium: rgba(15, 23, 42, 0.15);
|
|
||||||
--border-strong: rgba(15, 23, 42, 0.25);
|
|
||||||
--border-focus: #0284c7;
|
|
||||||
--border-highlight: rgba(2, 132, 199, 0.35);
|
|
||||||
|
|
||||||
/* High-end Subtle Shadows */
|
|
||||||
--shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.03), 0 0 0 1px rgba(15, 23, 42, 0.07);
|
|
||||||
--shadow-card-hover: 0 4px 12px -2px rgba(15, 23, 42, 0.08), 0 0 0 1px rgba(15, 23, 42, 0.1);
|
|
||||||
--shadow-popup: 0 20px 25px -5px rgba(15, 23, 42, 0.1), 0 8px 10px -6px rgba(15, 23, 42, 0.06), 0 0 0 1px rgba(15, 23, 42, 0.09);
|
|
||||||
--shadow-btn: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
/* Typography Colors (High Contrast) */
|
|
||||||
--text-primary: #0f172a;
|
|
||||||
--text-secondary: #475569;
|
|
||||||
--text-muted: #64748b;
|
|
||||||
--text-disabled: #94a3b8;
|
|
||||||
--text-inverse: #ffffff;
|
|
||||||
|
|
||||||
/* Accent Colors: DotB/Omni light theme. Bright blues are for large accents
|
|
||||||
and focus decoration only; small text uses the strong/dark variants. */
|
|
||||||
--accent-cyan: #0284c7;
|
|
||||||
--accent-cyan-hover: #0369a1;
|
|
||||||
--accent-cyan-subtle: rgba(2, 132, 199, 0.08);
|
|
||||||
--accent-indigo: #4f46e5;
|
|
||||||
--accent-indigo-subtle: rgba(79, 70, 229, 0.08);
|
|
||||||
--accent-blue: #1f93ff;
|
|
||||||
--accent-blue-strong: #075985;
|
|
||||||
--accent-blue-dark: #075985;
|
|
||||||
--accent-blue-deep: #0c4a6e;
|
|
||||||
--accent-blue-subtle: rgba(31, 147, 255, 0.08);
|
|
||||||
--accent-blue-border: #bae6fd;
|
|
||||||
--focus-ring: rgba(31, 147, 255, 0.35);
|
|
||||||
|
|
||||||
/* Status Colors: text variants meet WCAG AA on light surfaces. */
|
|
||||||
--status-good: #047857;
|
|
||||||
--status-good-text: #047857;
|
|
||||||
--status-good-bg: #ecfdf5;
|
|
||||||
--status-good-border: #a7f3d0;
|
|
||||||
|
|
||||||
--status-warning: #92400e;
|
|
||||||
--status-warning-text: #92400e;
|
|
||||||
--status-warning-bg: #fffbeb;
|
|
||||||
--status-warning-border: #fde68a;
|
|
||||||
|
|
||||||
--status-critical: #be123c;
|
|
||||||
--status-critical-text: #be123c;
|
|
||||||
--status-critical-bg: #fff1f2;
|
|
||||||
--status-critical-border: #fecdd3;
|
|
||||||
|
|
||||||
--status-info: #075985;
|
|
||||||
--status-info-bg: #f0f9ff;
|
|
||||||
--status-info-border: #bae6fd;
|
|
||||||
|
|
||||||
/* Font Families: Inter + System UI fallback (Aligned with DotB-Omni) */
|
|
||||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
|
||||||
|
|
||||||
/* Radii */
|
|
||||||
--radius-xs: 4px;
|
|
||||||
--radius-sm: 6px;
|
|
||||||
--radius-md: 10px;
|
|
||||||
--radius-lg: 14px;
|
|
||||||
--radius-xl: 20px;
|
|
||||||
--radius-full: 9999px;
|
|
||||||
|
|
||||||
/* Transitions */
|
|
||||||
--transition-fast: 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
--transition-smooth: 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
--transition-spring: 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
*, *::before, *::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
background-color: var(--bg-canvas);
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.5;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code, pre, kbd, samp, .font-mono {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-feature-settings: "tnum" 1, "zero" 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-family: inherit;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom crisp scrollbars */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(15, 23, 42, 0.15);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(15, 23, 42, 0.3);
|
|
||||||
}
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
import createClient from 'openapi-fetch';
|
|
||||||
import type { paths } from './schema.d';
|
|
||||||
import type {
|
|
||||||
CreateScoringRunRequest,
|
|
||||||
CreateScoringRunResponse,
|
|
||||||
DevConsoleConfig,
|
|
||||||
ListScoringRunsResult,
|
|
||||||
ScoringConfig,
|
|
||||||
ScoringRunResponse,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: DevConsoleConfig = {
|
|
||||||
tenantId: 'omni-account-42',
|
|
||||||
dotbApp: 'dev-console',
|
|
||||||
dotbFeature: 'lead-scoring',
|
|
||||||
pollIntervalMs: 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createApiClient() {
|
|
||||||
return createClient<paths>({
|
|
||||||
baseUrl: '/dev-console-api',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestHeaders {
|
|
||||||
tenantId?: string;
|
|
||||||
idempotencyKey?: string;
|
|
||||||
dotbApp?: string;
|
|
||||||
dotbFeature?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function admitScoringRun(
|
|
||||||
client: ReturnType<typeof createApiClient>,
|
|
||||||
payload: CreateScoringRunRequest,
|
|
||||||
headers: RequestHeaders = {},
|
|
||||||
config: DevConsoleConfig = DEFAULT_CONFIG,
|
|
||||||
): Promise<{ data?: CreateScoringRunResponse; error?: unknown; status: number }> {
|
|
||||||
const idemKey = headers.idempotencyKey ?? crypto.randomUUID();
|
|
||||||
const app = headers.dotbApp ?? config.dotbApp;
|
|
||||||
const feature = headers.dotbFeature ?? config.dotbFeature;
|
|
||||||
|
|
||||||
const res = await client.POST('/v1/scoring-runs', {
|
|
||||||
params: {
|
|
||||||
header: {
|
|
||||||
'X-Tenant-VKey': '',
|
|
||||||
'Idempotency-Key': idemKey,
|
|
||||||
'X-DotB-App': app,
|
|
||||||
'X-DotB-Feature': feature,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: res.data,
|
|
||||||
error: res.error,
|
|
||||||
status: res.response.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchScoringRun(
|
|
||||||
client: ReturnType<typeof createApiClient>,
|
|
||||||
runId: string,
|
|
||||||
tenantId: string = DEFAULT_CONFIG.tenantId,
|
|
||||||
): Promise<{ data?: ScoringRunResponse; error?: unknown; status: number }> {
|
|
||||||
const res = await client.GET('/v1/scoring-runs/{run_id}', {
|
|
||||||
params: {
|
|
||||||
path: { run_id: runId },
|
|
||||||
header: {
|
|
||||||
'X-Tenant-Id': tenantId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: res.data,
|
|
||||||
error: res.error,
|
|
||||||
status: res.response.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listScoringRuns(
|
|
||||||
client: ReturnType<typeof createApiClient>,
|
|
||||||
tenantId: string = DEFAULT_CONFIG.tenantId,
|
|
||||||
limit: number = 50,
|
|
||||||
offset: number = 0,
|
|
||||||
): Promise<ListScoringRunsResult> {
|
|
||||||
const res = await client.GET('/v1/scoring-runs', {
|
|
||||||
params: {
|
|
||||||
header: {
|
|
||||||
'X-Tenant-Id': tenantId,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.data) throw new Error(`Không tải được danh sách phiên chấm điểm: HTTP ${res.response?.status}`);
|
|
||||||
const headerTotal =
|
|
||||||
res.response?.headers?.get?.('x-total-count') ??
|
|
||||||
res.response?.headers?.get?.('X-Total-Count');
|
|
||||||
const parsedTotal = headerTotal ? parseInt(headerTotal, 10) : NaN;
|
|
||||||
const total = Number.isNaN(parsedTotal) ? res.data.length : parsedTotal;
|
|
||||||
return {
|
|
||||||
items: res.data,
|
|
||||||
total,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listScoringConfigs(
|
|
||||||
client: ReturnType<typeof createApiClient>,
|
|
||||||
config: DevConsoleConfig = DEFAULT_CONFIG,
|
|
||||||
): Promise<ScoringConfig[]> {
|
|
||||||
const res = await client.GET('/v1/scoring-runs/configs', {
|
|
||||||
params: {
|
|
||||||
header: {
|
|
||||||
'X-Tenant-Id': config.tenantId,
|
|
||||||
'X-DotB-App': config.dotbApp,
|
|
||||||
'X-DotB-Feature': config.dotbFeature,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.data) throw new Error(`Không tải được cấu hình: HTTP ${res.response.status}`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveScoringConfig(
|
|
||||||
client: ReturnType<typeof createApiClient>,
|
|
||||||
scoringConfig: ScoringConfig,
|
|
||||||
config: DevConsoleConfig = DEFAULT_CONFIG,
|
|
||||||
): Promise<ScoringConfig> {
|
|
||||||
const res = await client.POST('/v1/scoring-runs/configs', {
|
|
||||||
params: {
|
|
||||||
header: {
|
|
||||||
'X-Tenant-Id': config.tenantId,
|
|
||||||
'X-DotB-App': config.dotbApp,
|
|
||||||
'X-DotB-Feature': config.dotbFeature,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: scoringConfig,
|
|
||||||
});
|
|
||||||
if (res.response.status === 409) {
|
|
||||||
throw new Error(
|
|
||||||
`Phiên bản "${scoringConfig.version}" đã tồn tại. Mỗi cấu hình là bất biến (immutable), vui lòng đổi tên phiên bản mới (hoặc bấm "Tạo mã phiên bản mới").`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!res.data) {
|
|
||||||
const errorDetail =
|
|
||||||
(res.error as Record<string, unknown>)?.detail ??
|
|
||||||
JSON.stringify(res.error) ??
|
|
||||||
`HTTP ${res.response.status}`;
|
|
||||||
throw new Error(`Không lưu được cấu hình: ${String(errorDetail)}`);
|
|
||||||
}
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchExecutionTrace(
|
|
||||||
client: ReturnType<typeof createApiClient>,
|
|
||||||
runId: string,
|
|
||||||
tenantId: string,
|
|
||||||
): Promise<Record<string, unknown>> {
|
|
||||||
const res = await client.GET('/v1/scoring-runs/{run_id}/trace', {
|
|
||||||
params: { path: { run_id: runId }, header: { 'X-Tenant-Id': tenantId } },
|
|
||||||
});
|
|
||||||
if (!res.data) throw new Error(`Không tải được pipeline trace: HTTP ${res.response.status}`);
|
|
||||||
return res.data as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TERMINAL_STATUSES = new Set([
|
|
||||||
'completed',
|
|
||||||
'review_required',
|
|
||||||
'failed',
|
|
||||||
'superseded',
|
|
||||||
]);
|
|
||||||
|
|
||||||
export async function pollUntilTerminal(
|
|
||||||
client: ReturnType<typeof createApiClient>,
|
|
||||||
runId: string,
|
|
||||||
options: {
|
|
||||||
tenantId?: string;
|
|
||||||
intervalMs?: number;
|
|
||||||
maxAttempts?: number;
|
|
||||||
onTick?: (run: ScoringRunResponse) => void;
|
|
||||||
} = {},
|
|
||||||
): Promise<ScoringRunResponse> {
|
|
||||||
const intervalMs = options.intervalMs ?? DEFAULT_CONFIG.pollIntervalMs;
|
|
||||||
const maxAttempts = options.maxAttempts ?? 30;
|
|
||||||
const tenantId = options.tenantId ?? DEFAULT_CONFIG.tenantId;
|
|
||||||
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
|
||||||
const res = await fetchScoringRun(client, runId, tenantId);
|
|
||||||
if (!res.data) {
|
|
||||||
throw new Error(`Failed to fetch run: ${JSON.stringify(res.error)} (HTTP ${res.status})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.onTick) {
|
|
||||||
options.onTick(res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TERMINAL_STATUSES.has(res.data.status)) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Polling timed out after ${maxAttempts} attempts for run ${runId}`);
|
|
||||||
}
|
|
||||||
1122
old_repo/apps/dev-console/src/lib/api/schema.d.ts
vendored
@ -1,38 +0,0 @@
|
|||||||
import type { components, paths } from './schema.d';
|
|
||||||
|
|
||||||
export type CreateScoringRunRequest = components['schemas']['CreateScoringRunRequest'];
|
|
||||||
export type CreateScoringRunResponse = components['schemas']['CreateScoringRunResponse'];
|
|
||||||
export type ScoringRunResponse = components['schemas']['ScoringRunResponse'];
|
|
||||||
export type CanonicalSnapshot = components['schemas']['CanonicalSnapshot'];
|
|
||||||
export type ScoringConfig = components['schemas']['ScoringConfig'];
|
|
||||||
export type BehavioralMetric = components['schemas']['BehavioralMetric'];
|
|
||||||
export type DynamicRule = components['schemas']['DynamicRule'];
|
|
||||||
export type DynamicRuleOperator = components['schemas']['DynamicRuleOperator'];
|
|
||||||
export type DynamicRuleTarget = components['schemas']['DynamicRuleTarget'];
|
|
||||||
export type ScoringRunStatus = components['schemas']['ScoringRunStatus'];
|
|
||||||
export type ErrorResponse = components['schemas']['ErrorResponse'];
|
|
||||||
export type AiMetadataSuccess = components['schemas']['AiMetadataSuccess'];
|
|
||||||
export type AiMetadataFailure = components['schemas']['AiMetadataFailure'];
|
|
||||||
|
|
||||||
export interface DevConsoleConfig {
|
|
||||||
tenantId: string;
|
|
||||||
dotbApp: string;
|
|
||||||
dotbFeature: string;
|
|
||||||
pollIntervalMs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListScoringRunsResult {
|
|
||||||
items: ScoringRunResponse[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GoldenFixture {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
expectedStatus: ScoringRunStatus;
|
|
||||||
expectedMode?: string;
|
|
||||||
category?: string;
|
|
||||||
coverImage?: string;
|
|
||||||
payload: CreateScoringRunRequest;
|
|
||||||
}
|
|
||||||
@ -1,302 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from './Icon.svelte';
|
|
||||||
|
|
||||||
const plannedSignals = [
|
|
||||||
{ title: 'Tần suất điểm danh & Nghỉ học', desc: 'Số buổi nghỉ liên tiếp, tỷ lệ đi học giảm sút so với tháng trước từ DOTB EMS.', weight: '35%' },
|
|
||||||
{ title: 'Tiến độ học tập & Điểm số LMS', desc: 'Điểm bài kiểm tra định kỳ, tỷ lệ nộp bài tập về nhà và thời gian truy cập portal.', weight: '30%' },
|
|
||||||
{ title: 'Tương tác phụ huynh & Đóng phí', desc: 'Độ trễ đóng học phí tái tục, cảm xúc tin nhắn phàn nàn trên kênh Zalo/Facebook.', weight: '25%' },
|
|
||||||
{ title: 'Biến động giáo viên & Lớp học', desc: 'Lớp thay đổi giáo viên hoặc ghép lớp làm giảm trải nghiệm học tập của học viên.', weight: '10%' },
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="churn-container">
|
|
||||||
<div class="panel churn-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<div>
|
|
||||||
<div class="status-badge-row">
|
|
||||||
<span class="status-badge dev">In Development</span>
|
|
||||||
<span class="phase-tag">Dự án giai đoạn tiếp theo · Q4/2026</span>
|
|
||||||
</div>
|
|
||||||
<h2>Dự đoán học viên rời bỏ (Student Churn Prediction)</h2>
|
|
||||||
<p class="panel-desc">Mô hình AI dự báo sớm nguy cơ học viên nghỉ học hoặc không tái tục trước 30 ngày để can thiệp kịp thời.</p>
|
|
||||||
</div>
|
|
||||||
<div class="target-crm-tag">
|
|
||||||
<span>Tích hợp:</span>
|
|
||||||
<code>DOTB EMS · LMS · Omni</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sơ đồ quy trình dự báo -->
|
|
||||||
<div class="workflow-preview">
|
|
||||||
<div class="workflow-step">
|
|
||||||
<span class="step-num">01</span>
|
|
||||||
<strong>Thu thập tín hiệu</strong>
|
|
||||||
<p>Hệ thống tự động đồng bộ sự kiện điểm danh, kết quả bài tập và hội thoại từ EMS & Omni.</p>
|
|
||||||
</div>
|
|
||||||
<div class="workflow-arrow">→</div>
|
|
||||||
<div class="workflow-step">
|
|
||||||
<span class="step-num">02</span>
|
|
||||||
<strong>Mô hình AI phân tích</strong>
|
|
||||||
<p>Mô hình kết hợp ML phân tích chuỗi thời gian + LLM đánh giá cảm xúc phụ huynh.</p>
|
|
||||||
</div>
|
|
||||||
<div class="workflow-arrow">→</div>
|
|
||||||
<div class="workflow-step highlight">
|
|
||||||
<span class="step-num">03</span>
|
|
||||||
<strong>Cảnh báo & Can thiệp</strong>
|
|
||||||
<p>Gắn nhãn rủi ro Churn (High / Medium / Low) và tự động tạo việc cho tư vấn viên chăm sóc.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bảng trọng số tín hiệu dự kiến -->
|
|
||||||
<div class="signals-section">
|
|
||||||
<h3>Bộ chỉ số tín hiệu dự báo sớm</h3>
|
|
||||||
<div class="signals-grid">
|
|
||||||
{#each plannedSignals as signal}
|
|
||||||
<div class="signal-card">
|
|
||||||
<div class="signal-header">
|
|
||||||
<strong>{signal.title}</strong>
|
|
||||||
<span class="signal-weight">{signal.weight}</span>
|
|
||||||
</div>
|
|
||||||
<p>{signal.desc}</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thông báo lộ trình -->
|
|
||||||
<div class="roadmap-notice">
|
|
||||||
<div class="roadmap-icon">
|
|
||||||
<Icon name="git-branch" size={18} />
|
|
||||||
</div>
|
|
||||||
<div class="roadmap-text">
|
|
||||||
<strong>Lộ trình triển khai</strong>
|
|
||||||
<p>Tính năng này sẽ được phát triển tiếp nối sau khi hoàn tất giai đoạn Customer Pilot của phân hệ Lead Scoring. Bộ API và Webhook sẽ kế thừa chuẩn kiến trúc bảo mật dùng chung của AI Platform.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.churn-container {
|
|
||||||
max-width: 1440px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 24px 28px 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.churn-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 28px;
|
|
||||||
padding: 32px;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.dev {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
border: 1px solid #fde68a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phase-tag {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.76rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header h2 {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 1.4rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-desc {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.88rem;
|
|
||||||
max-width: 70ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-crm-tag {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-crm-tag code {
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Workflow Preview */
|
|
||||||
.workflow-preview {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr auto 1fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflow-step {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflow-step.highlight {
|
|
||||||
border-color: var(--accent-blue);
|
|
||||||
background: var(--accent-blue-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-num {
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflow-step strong {
|
|
||||||
font-size: 0.88rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflow-step p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflow-arrow {
|
|
||||||
color: var(--border-medium);
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Signals Section */
|
|
||||||
.signals-section h3 {
|
|
||||||
margin: 0 0 16px;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signals-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signal-card {
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signal-header strong {
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signal-weight {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
padding: 2px 6px;
|
|
||||||
background: var(--accent-blue-subtle);
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signal-card p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Roadmap Notice */
|
|
||||||
.roadmap-notice {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-left: 3px solid var(--accent-blue);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.roadmap-icon {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roadmap-text strong {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roadmap-text p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.workflow-preview {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.workflow-arrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.signals-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,616 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from './Icon.svelte';
|
|
||||||
import type { DynamicRule, DynamicRuleOperator, DynamicRuleTarget } from '../api/types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
rules: DynamicRule[];
|
|
||||||
sampleData?: {
|
|
||||||
studentAge?: number;
|
|
||||||
leadSource?: string;
|
|
||||||
courseInterest?: string;
|
|
||||||
studyArea?: string;
|
|
||||||
customFields?: Record<string, any>;
|
|
||||||
contactRecencyDays?: number;
|
|
||||||
conversationCount?: number;
|
|
||||||
messageCount?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let { rules = $bindable([]), sampleData = {} }: Props = $props();
|
|
||||||
|
|
||||||
const targetOptions: { value: DynamicRuleTarget; label: string; isCustom?: boolean }[] = [
|
|
||||||
{ value: 'student_age', label: 'Tuổi học viên (student_age)' },
|
|
||||||
{ value: 'lead_source', label: 'Nguồn tiếp cận (lead_source)' },
|
|
||||||
{ value: 'course_interest', label: 'Khóa học quan tâm (course_interest)' },
|
|
||||||
{ value: 'study_area', label: 'Khu vực / Cơ sở (study_area)' },
|
|
||||||
{ value: 'contact_message_recency_days', label: 'Số ngày từ tin nhắn gần nhất' },
|
|
||||||
{ value: 'conversation_count', label: 'Số cuộc hội thoại' },
|
|
||||||
{ value: 'message_count', label: 'Số tin nhắn tương tác' },
|
|
||||||
{ value: 'custom_field', label: 'Thuộc tính mở rộng CRM (custom_fields)', isCustom: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
const operatorOptions: { value: DynamicRuleOperator; label: string }[] = [
|
|
||||||
{ value: 'gte', label: '≥ (Lớn hơn hoặc bằng)' },
|
|
||||||
{ value: 'gt', label: '> (Lớn hơn)' },
|
|
||||||
{ value: 'lte', label: '≤ (Nhỏ hơn hoặc bằng)' },
|
|
||||||
{ value: 'lt', label: '< (Nhỏ hơn)' },
|
|
||||||
{ value: 'eq', label: '= (Bằng chính xác)' },
|
|
||||||
{ value: 'neq', label: '≠ (Khác)' },
|
|
||||||
{ value: 'contains', label: 'chứa (Contains text)' },
|
|
||||||
{ value: 'in', label: 'nằm trong danh sách (In)' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const presets = [
|
|
||||||
{
|
|
||||||
name: 'Ngân sách trên 5 triệu',
|
|
||||||
target: 'custom_field' as DynamicRuleTarget,
|
|
||||||
custom_field_key: 'budget',
|
|
||||||
operator: 'gte' as DynamicRuleOperator,
|
|
||||||
value: 5000000,
|
|
||||||
points: 15,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Cơ sở Quận 3',
|
|
||||||
target: 'study_area' as DynamicRuleTarget,
|
|
||||||
operator: 'contains' as DynamicRuleOperator,
|
|
||||||
value: 'Quận 3',
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Nhu cầu học cấp bách',
|
|
||||||
target: 'custom_field' as DynamicRuleTarget,
|
|
||||||
custom_field_key: 'urgency',
|
|
||||||
operator: 'in' as DynamicRuleOperator,
|
|
||||||
value: 'high, urgent, gap',
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Học viên 6 đến 12 tuổi',
|
|
||||||
target: 'student_age' as DynamicRuleTarget,
|
|
||||||
operator: 'gte' as DynamicRuleOperator,
|
|
||||||
value: 6,
|
|
||||||
points: 10,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function generateRuleId() {
|
|
||||||
return `rule_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEmptyRule() {
|
|
||||||
const newId = generateRuleId();
|
|
||||||
rules = [
|
|
||||||
...rules,
|
|
||||||
{
|
|
||||||
id: newId,
|
|
||||||
name: `Quy tắc mới #${rules.length + 1}`,
|
|
||||||
target: 'custom_field',
|
|
||||||
custom_field_key: 'budget',
|
|
||||||
operator: 'gte',
|
|
||||||
value: '5000000',
|
|
||||||
points: 10,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyPreset(preset: (typeof presets)[0]) {
|
|
||||||
const newId = generateRuleId();
|
|
||||||
rules = [
|
|
||||||
...rules,
|
|
||||||
{
|
|
||||||
id: newId,
|
|
||||||
name: preset.name,
|
|
||||||
target: preset.target,
|
|
||||||
custom_field_key: preset.custom_field_key,
|
|
||||||
operator: preset.operator,
|
|
||||||
value: preset.value,
|
|
||||||
points: preset.points,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteRule(index: number) {
|
|
||||||
rules = rules.filter((_, i) => i !== index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripAccents(s: string): string {
|
|
||||||
return s
|
|
||||||
.replace(/đ/g, 'd')
|
|
||||||
.replace(/Đ/g, 'd')
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[̀-ͯ]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client-side quick preview of rule against sampleData
|
|
||||||
function evaluateRuleLive(rule: DynamicRule): { status: 'matched' | 'not_matched' | 'missing' | 'disabled'; detail: string } {
|
|
||||||
if (!rule.enabled) return { status: 'disabled', detail: 'Tạm tắt' };
|
|
||||||
|
|
||||||
let actualVal: any = undefined;
|
|
||||||
if (rule.target === 'student_age') actualVal = sampleData.studentAge;
|
|
||||||
else if (rule.target === 'lead_source') actualVal = sampleData.leadSource;
|
|
||||||
else if (rule.target === 'course_interest') actualVal = sampleData.courseInterest;
|
|
||||||
else if (rule.target === 'study_area') actualVal = sampleData.studyArea;
|
|
||||||
else if (rule.target === 'contact_message_recency_days') actualVal = sampleData.contactRecencyDays;
|
|
||||||
else if (rule.target === 'conversation_count') actualVal = sampleData.conversationCount ?? 1;
|
|
||||||
else if (rule.target === 'message_count') actualVal = sampleData.messageCount ?? 1;
|
|
||||||
else if (rule.target === 'custom_field' && rule.custom_field_key) {
|
|
||||||
actualVal = sampleData.customFields?.[rule.custom_field_key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actualVal === undefined || actualVal === null || actualVal === '') {
|
|
||||||
return { status: 'missing', detail: 'Thiếu dữ liệu trên Lead mẫu' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const actStr = String(actualVal).trim().toLowerCase();
|
|
||||||
const tgtStr = String(rule.value).trim().toLowerCase();
|
|
||||||
const actStripped = stripAccents(actStr);
|
|
||||||
const tgtStripped = stripAccents(tgtStr);
|
|
||||||
|
|
||||||
const actNum = Number(actualVal);
|
|
||||||
const tgtNum = Number(rule.value);
|
|
||||||
|
|
||||||
if (['gt', 'gte', 'lt', 'lte'].includes(rule.operator) && !isNaN(actNum) && !isNaN(tgtNum)) {
|
|
||||||
let isMatch = false;
|
|
||||||
if (rule.operator === 'gt') isMatch = actNum > tgtNum;
|
|
||||||
if (rule.operator === 'gte') isMatch = actNum >= tgtNum;
|
|
||||||
if (rule.operator === 'lt') isMatch = actNum < tgtNum;
|
|
||||||
if (rule.operator === 'lte') isMatch = actNum <= tgtNum;
|
|
||||||
return isMatch
|
|
||||||
? { status: 'matched', detail: `Khớp: ${actualVal} ${rule.operator} ${rule.value}` }
|
|
||||||
: { status: 'not_matched', detail: `Không khớp: ${actualVal}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.operator === 'eq') {
|
|
||||||
const isMatch = actStr === tgtStr || actStripped === tgtStripped;
|
|
||||||
return isMatch
|
|
||||||
? { status: 'matched', detail: `Khớp: '${actualVal}'` }
|
|
||||||
: { status: 'not_matched', detail: `Khác: '${actualVal}'` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.operator === 'neq') {
|
|
||||||
const isMatch = actStr !== tgtStr && actStripped !== tgtStripped;
|
|
||||||
return isMatch
|
|
||||||
? { status: 'matched', detail: `Khác: '${actualVal}' ≠ '${rule.value}'` }
|
|
||||||
: { status: 'not_matched', detail: `Trùng: '${actualVal}'` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.operator === 'contains') {
|
|
||||||
const isMatch = actStr.includes(tgtStr) || actStripped.includes(tgtStripped);
|
|
||||||
return isMatch
|
|
||||||
? { status: 'matched', detail: `Chứa '${rule.value}'` }
|
|
||||||
: { status: 'not_matched', detail: `Không chứa '${rule.value}'` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.operator === 'in') {
|
|
||||||
const items = tgtStr.split(',').map((s) => s.trim());
|
|
||||||
const isMatch = items.some(
|
|
||||||
(item) => item === actStr || stripAccents(item) === actStripped
|
|
||||||
);
|
|
||||||
return isMatch
|
|
||||||
? { status: 'matched', detail: `Nằm trong danh sách` }
|
|
||||||
: { status: 'not_matched', detail: `Không thuộc danh sách` };
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return { status: 'not_matched', detail: 'Lỗi so sánh dữ liệu' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { status: 'not_matched', detail: 'Không khớp' };
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalLiveBonus = $derived(
|
|
||||||
rules.reduce((acc, r) => {
|
|
||||||
const res = evaluateRuleLive(r);
|
|
||||||
return res.status === 'matched' ? acc + r.points : acc;
|
|
||||||
}, 0),
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="dynamic-rules-wrap">
|
|
||||||
<div class="rules-header-row">
|
|
||||||
<div>
|
|
||||||
<h4 class="rules-title">Customer rules mô phỏng request Omni</h4>
|
|
||||||
<p class="rules-desc">
|
|
||||||
Chỉ dùng cho phiên thử nghiệm hiện tại, mô phỏng rule_overrides do Omni gửi và không lưu vào AI config.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="rules-header-actions">
|
|
||||||
{#if sampleData && Object.keys(sampleData).length > 0}
|
|
||||||
<span class="live-bonus-chip" class:positive={totalLiveBonus > 0}>
|
|
||||||
Test trên Lead mẫu: <strong>+{totalLiveBonus}đ</strong>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<button class="add-rule-btn" type="button" onclick={addEmptyRule}>
|
|
||||||
<Icon name="plus" size={13} />
|
|
||||||
<span>Thêm quy tắc</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preset Suggestions -->
|
|
||||||
<div class="presets-bar">
|
|
||||||
<span class="preset-label">Mẫu gợi ý:</span>
|
|
||||||
{#each presets as preset}
|
|
||||||
<button class="preset-chip" type="button" onclick={() => applyPreset(preset)}>
|
|
||||||
+ {preset.name} (+{preset.points}đ)
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rules List -->
|
|
||||||
{#if rules.length === 0}
|
|
||||||
<div class="empty-rules-box">
|
|
||||||
<Icon name="sliders" size={20} class="text-muted" />
|
|
||||||
<p>Chưa có quy tắc mở rộng nào. Bấm <strong>"Thêm quy tắc"</strong> hoặc chọn mẫu gợi ý phía trên.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="rules-table-wrap">
|
|
||||||
<table class="rules-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 40px;">Bật</th>
|
|
||||||
<th style="width: 180px;">Tên quy tắc</th>
|
|
||||||
<th style="width: 220px;">Thuộc tính mục tiêu</th>
|
|
||||||
<th style="width: 160px;">Toán tử</th>
|
|
||||||
<th style="width: 140px;">Giá trị so sánh</th>
|
|
||||||
<th style="width: 80px;">Điểm</th>
|
|
||||||
<th>Kiểm thử Lead mẫu</th>
|
|
||||||
<th style="width: 40px;"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each rules as rule, index (rule.id || index)}
|
|
||||||
{@const testRes = evaluateRuleLive(rule)}
|
|
||||||
<tr class:rule-disabled={!rule.enabled}>
|
|
||||||
<td class="cell-center">
|
|
||||||
<input type="checkbox" bind:checked={rule.enabled} aria-label="Bật/Tắt quy tắc" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input class="rule-input name-input" type="text" bind:value={rule.name} placeholder="Tên quy tắc..." />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="target-field-stack">
|
|
||||||
<select class="rule-select" bind:value={rule.target}>
|
|
||||||
{#each targetOptions as opt}
|
|
||||||
<option value={opt.value}>{opt.label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
{#if rule.target === 'custom_field'}
|
|
||||||
<input
|
|
||||||
class="rule-input custom-key-input"
|
|
||||||
type="text"
|
|
||||||
bind:value={rule.custom_field_key}
|
|
||||||
placeholder="Tên custom_field (ví dụ: budget, urgency)..."
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select class="rule-select" bind:value={rule.operator}>
|
|
||||||
{#each operatorOptions as op}
|
|
||||||
<option value={op.value}>{op.label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input class="rule-input" type="text" bind:value={rule.value} placeholder="Giá trị..." />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input class="rule-input points-input" type="number" min="-100" max="100" bind:value={rule.points} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="live-test-cell">
|
|
||||||
{#if testRes.status === 'matched'}
|
|
||||||
<span class="test-badge matched" title={testRes.detail}>
|
|
||||||
<Icon name="check" size={12} />
|
|
||||||
<span>+{rule.points}đ</span>
|
|
||||||
</span>
|
|
||||||
{:else if testRes.status === 'not_matched'}
|
|
||||||
<span class="test-badge not-matched" title={testRes.detail}>0đ</span>
|
|
||||||
{:else if testRes.status === 'missing'}
|
|
||||||
<span class="test-badge missing" title={testRes.detail}>Chưa có</span>
|
|
||||||
{:else}
|
|
||||||
<span class="test-badge disabled">Tắt</span>
|
|
||||||
{/if}
|
|
||||||
<span class="test-detail-text">{testRes.detail}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="cell-center">
|
|
||||||
<button
|
|
||||||
class="del-btn"
|
|
||||||
type="button"
|
|
||||||
title="Xóa quy tắc"
|
|
||||||
onclick={() => deleteRule(index)}
|
|
||||||
>
|
|
||||||
<Icon name="trash" size={13} />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.dynamic-rules-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-header-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-title {
|
|
||||||
margin: 0 0 2px;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-desc {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-bonus-chip {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-bonus-chip.positive {
|
|
||||||
background: #ecfdf5;
|
|
||||||
color: #047857;
|
|
||||||
border-color: #a7f3d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-rule-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--accent-blue-subtle);
|
|
||||||
border: 1px solid var(--accent-blue-border);
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-rule-btn:hover {
|
|
||||||
background: var(--bg-surface-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Presets Bar */
|
|
||||||
.presets-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
border: 1px dashed var(--border-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-chip {
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-chip:hover {
|
|
||||||
border-color: var(--accent-blue);
|
|
||||||
color: var(--accent-blue-strong);
|
|
||||||
background: var(--accent-blue-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-rules-box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-rules-box p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table */
|
|
||||||
.rules-table-wrap {
|
|
||||||
overflow-x: auto;
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-table th {
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
padding: 8px 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-table td {
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rules-table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-disabled td {
|
|
||||||
opacity: 0.55;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-input,
|
|
||||||
.rule-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px 8px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
background: #ffffff;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-input:focus,
|
|
||||||
.rule-select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.points-input {
|
|
||||||
width: 65px;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-field-stack {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-key-input {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Live test cell */
|
|
||||||
.live-test-cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-badge.matched {
|
|
||||||
background: #ecfdf5;
|
|
||||||
color: #047857;
|
|
||||||
border: 1px solid #a7f3d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-badge.not-matched {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #64748b;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-badge.missing {
|
|
||||||
background: #fffbeb;
|
|
||||||
color: #b45309;
|
|
||||||
border: 1px solid #fde68a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-badge.disabled {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-detail-text {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.del-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.del-btn:hover {
|
|
||||||
color: var(--status-critical);
|
|
||||||
background: var(--status-critical-bg);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,518 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { GOLDEN_FIXTURES } from '../fixtures/golden';
|
|
||||||
import { consoleState } from '../state/consoleState.svelte';
|
|
||||||
import Icon from './Icon.svelte';
|
|
||||||
|
|
||||||
let isEditingJson = $state(false);
|
|
||||||
let jsonError = $state<string | null>(null);
|
|
||||||
let copiedPayload = $state(false);
|
|
||||||
|
|
||||||
// Validate JSON on the fly
|
|
||||||
$effect(() => {
|
|
||||||
try {
|
|
||||||
JSON.parse(consoleState.customPayloadJson);
|
|
||||||
jsonError = null;
|
|
||||||
} catch (e) {
|
|
||||||
jsonError = e instanceof Error ? e.message : 'Invalid JSON syntax';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatJson() {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(consoleState.customPayloadJson);
|
|
||||||
consoleState.customPayloadJson = JSON.stringify(parsed, null, 2);
|
|
||||||
jsonError = null;
|
|
||||||
} catch {
|
|
||||||
jsonError = 'Cannot format invalid JSON';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetToFixture() {
|
|
||||||
consoleState.customPayloadJson = JSON.stringify(consoleState.selectedFixture.payload, null, 2);
|
|
||||||
jsonError = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyPayload() {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(consoleState.customPayloadJson);
|
|
||||||
copiedPayload = true;
|
|
||||||
setTimeout(() => (copiedPayload = false), 2000);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="fixture-panel">
|
|
||||||
<!-- Card Header -->
|
|
||||||
<div class="fixture-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<div class="icon-bubble">
|
|
||||||
<Icon name="layers" size={17} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="panel-title">Kịch bản kiểm thử chuẩn</h3>
|
|
||||||
<span class="panel-subtitle">Bộ dữ liệu lead mẫu và ảnh chụp hội thoại chuẩn</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn-toggle-editor {isEditingJson ? 'active' : ''}"
|
|
||||||
onclick={() => (isEditingJson = !isEditingJson)}
|
|
||||||
title="Bật tắt trình chỉnh sửa dữ liệu JSON"
|
|
||||||
>
|
|
||||||
<Icon name="sliders" size={13} />
|
|
||||||
<span>{isEditingJson ? 'Ẩn trình sửa' : 'Sửa payload'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scenario Catalog List with Architectural Thumbnails -->
|
|
||||||
<div class="scenario-list" role="radiogroup" aria-label="Danh sách kịch bản">
|
|
||||||
{#each GOLDEN_FIXTURES as fixture}
|
|
||||||
{@const isSelected = consoleState.selectedFixture.id === fixture.id}
|
|
||||||
<button
|
|
||||||
class="scenario-card {isSelected ? 'active' : ''}"
|
|
||||||
role="radio"
|
|
||||||
aria-checked={isSelected}
|
|
||||||
onclick={() => consoleState.selectFixture(fixture)}
|
|
||||||
>
|
|
||||||
<div class="scenario-layout">
|
|
||||||
{#if fixture.coverImage}
|
|
||||||
<div class="scenario-thumb-wrap">
|
|
||||||
<img src={fixture.coverImage} alt={fixture.name} class="scenario-thumb" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="scenario-details">
|
|
||||||
<div class="scenario-top">
|
|
||||||
<div class="scenario-cat-group">
|
|
||||||
<span class="scenario-cat font-mono">{fixture.category ?? 'KỊCH BẢN'}</span>
|
|
||||||
</div>
|
|
||||||
<span class="expect-pill status-{fixture.expectedStatus} font-mono">
|
|
||||||
{fixture.expectedStatus}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<strong class="scenario-name">{fixture.name}</strong>
|
|
||||||
<p class="scenario-desc">{fixture.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Payload Editor Drawer -->
|
|
||||||
{#if isEditingJson}
|
|
||||||
<div class="editor-drawer">
|
|
||||||
<div class="editor-bar">
|
|
||||||
<div class="editor-status">
|
|
||||||
{#if jsonError}
|
|
||||||
<span class="status-err font-mono">
|
|
||||||
<Icon name="alert-circle" size={12} />
|
|
||||||
<span>JSON không hợp lệ</span>
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-ok font-mono">
|
|
||||||
<Icon name="check" size={12} />
|
|
||||||
<span>Cấu trúc hợp lệ</span>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-tools">
|
|
||||||
<button class="btn-tool" onclick={formatJson} title="Căn chỉnh thụt dòng JSON">
|
|
||||||
Định dạng
|
|
||||||
</button>
|
|
||||||
<button class="btn-tool" onclick={resetToFixture} title="Đặt lại theo mặc định kịch bản">
|
|
||||||
Đặt lại
|
|
||||||
</button>
|
|
||||||
<button class="btn-tool" onclick={copyPayload} title="Sao chép JSON payload">
|
|
||||||
<Icon name={copiedPayload ? 'check' : 'copy'} size={11} />
|
|
||||||
<span>{copiedPayload ? 'Đã chép' : 'Sao chép'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
id="json-payload"
|
|
||||||
bind:value={consoleState.customPayloadJson}
|
|
||||||
rows="9"
|
|
||||||
spellcheck="false"
|
|
||||||
class="payload-textarea font-mono {jsonError ? 'error-ring' : ''}"
|
|
||||||
placeholder="Dán dữ liệu JSON payload..."
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
{#if jsonError}
|
|
||||||
<span class="err-hint font-mono">{jsonError}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Trigger Run Action -->
|
|
||||||
<button
|
|
||||||
class="btn-trigger {consoleState.isLoading ? 'loading' : ''}"
|
|
||||||
disabled={consoleState.isLoading || !!jsonError}
|
|
||||||
onclick={() => consoleState.triggerRun()}
|
|
||||||
>
|
|
||||||
{#if consoleState.isLoading}
|
|
||||||
<span class="spinner"></span>
|
|
||||||
<span>Đang tiếp nhận và theo dõi tiến trình...</span>
|
|
||||||
{:else}
|
|
||||||
<Icon name="play" size={15} />
|
|
||||||
<span>Chạy đánh giá điểm lead</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.fixture-panel {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 20px 24px;
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fixture-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
padding-bottom: 14px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-bubble {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--accent-cyan-subtle);
|
|
||||||
border: 1px solid var(--status-info-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.02rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-subtitle {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-toggle-editor {
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-toggle-editor:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--border-medium);
|
|
||||||
background: var(--bg-surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-toggle-editor.active {
|
|
||||||
background: var(--accent-cyan-subtle);
|
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-card {
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 10px 12px;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
color: var(--text-primary);
|
|
||||||
position: relative;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-card:hover {
|
|
||||||
border-color: var(--border-medium);
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-card.active {
|
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 0 0 1px var(--accent-cyan), 0 4px 12px rgba(2, 132, 199, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-thumb-wrap {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-thumb {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-card:hover .scenario-thumb {
|
|
||||||
transform: scale(1.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-top {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-cat-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-cat {
|
|
||||||
font-size: 0.62rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-name {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expect-pill {
|
|
||||||
font-size: 0.62rem;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expect-pill.status-completed {
|
|
||||||
background: var(--status-good-bg);
|
|
||||||
color: var(--status-good);
|
|
||||||
border: 1px solid var(--status-good-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.expect-pill.status-degraded {
|
|
||||||
background: var(--status-warning-bg);
|
|
||||||
color: var(--status-warning);
|
|
||||||
border: 1px solid var(--status-warning-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.expect-pill.status-failed {
|
|
||||||
background: var(--status-critical-bg);
|
|
||||||
color: var(--status-critical);
|
|
||||||
border: 1px solid var(--status-critical-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-desc {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.4;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-drawer {
|
|
||||||
background: #0f172a;
|
|
||||||
border: 1px solid #1e293b;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-ok {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #34d399;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-err {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #fb7185;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-tools {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-tool {
|
|
||||||
background: #1e293b;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
color: #cbd5e1;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-tool:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
border-color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payload-textarea {
|
|
||||||
background: #020617;
|
|
||||||
border: 1px solid #1e293b;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
color: #93c5fd;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
padding: 10px 12px;
|
|
||||||
resize: vertical;
|
|
||||||
width: 100%;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payload-textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.payload-textarea.error-ring {
|
|
||||||
border-color: var(--status-critical);
|
|
||||||
}
|
|
||||||
|
|
||||||
.err-hint {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: #fb7185;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-trigger {
|
|
||||||
background: linear-gradient(135deg, #0284c7, #0ea5e9);
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 13px 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(2, 132, 199, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-trigger:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(135deg, #0369a1, #0284c7);
|
|
||||||
box-shadow: 0 4px 14px rgba(2, 132, 199, 0.35);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-trigger:active:not(:disabled) {
|
|
||||||
transform: scale(0.99);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-trigger:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-trigger.loading {
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
border: 1px solid var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
border: 2px solid rgba(2, 132, 199, 0.3);
|
|
||||||
border-top-color: var(--accent-cyan);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,159 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
name:
|
|
||||||
| 'sparkles'
|
|
||||||
| 'play'
|
|
||||||
| 'refresh'
|
|
||||||
| 'check'
|
|
||||||
| 'check-circle'
|
|
||||||
| 'alert-triangle'
|
|
||||||
| 'alert-circle'
|
|
||||||
| 'sliders'
|
|
||||||
| 'code'
|
|
||||||
| 'copy'
|
|
||||||
| 'settings'
|
|
||||||
| 'chevron-down'
|
|
||||||
| 'chevron-right'
|
|
||||||
| 'chevron-up'
|
|
||||||
| 'activity'
|
|
||||||
| 'shield'
|
|
||||||
| 'git-branch'
|
|
||||||
| 'clock'
|
|
||||||
| 'layers'
|
|
||||||
| 'cpu'
|
|
||||||
| 'zap'
|
|
||||||
| 'crosshair'
|
|
||||||
| 'target'
|
|
||||||
| 'trending-down'
|
|
||||||
| 'book-open'
|
|
||||||
| 'file-text'
|
|
||||||
| 'plus'
|
|
||||||
| 'trash'
|
|
||||||
| 'close';
|
|
||||||
size?: number;
|
|
||||||
strokeWidth?: number;
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { name, size = 16, strokeWidth = 1.75, class: className = '' }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width={strokeWidth}
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="svg-icon {className}"
|
|
||||||
>
|
|
||||||
{#if name === 'sparkles'}
|
|
||||||
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
|
||||||
<path d="M5 3v4" /><path d="M19 17v4" /><path d="M3 5h4" /><path d="M17 19h4" />
|
|
||||||
{:else if name === 'play'}
|
|
||||||
<polygon points="6 3 20 12 6 21 6 3" />
|
|
||||||
{:else if name === 'refresh'}
|
|
||||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
|
||||||
<path d="M21 3v5h-5" />
|
|
||||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
|
||||||
<path d="M8 16H3v5" />
|
|
||||||
{:else if name === 'check'}
|
|
||||||
<polyline points="20 6 9 17 4 12" />
|
|
||||||
{:else if name === 'check-circle'}
|
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
||||||
<polyline points="22 4 12 14.01 9 11.01" />
|
|
||||||
{:else if name === 'alert-triangle'}
|
|
||||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
|
||||||
<line x1="12" y1="9" x2="12" y2="13" />
|
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
||||||
{:else if name === 'alert-circle'}
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<line x1="12" y1="8" x2="12" y2="12" />
|
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
||||||
{:else if name === 'sliders'}
|
|
||||||
<line x1="4" y1="21" x2="4" y2="14" /><line x1="4" y1="10" x2="4" y2="3" />
|
|
||||||
<line x1="12" y1="21" x2="12" y2="12" /><line x1="12" y1="8" x2="12" y2="3" />
|
|
||||||
<line x1="20" y1="21" x2="20" y2="16" /><line x1="20" y1="12" x2="20" y2="3" />
|
|
||||||
<line x1="1" y1="14" x2="7" y2="14" /><line x1="9" y1="8" x2="15" y2="8" />
|
|
||||||
<line x1="17" y1="16" x2="23" y2="16" />
|
|
||||||
{:else if name === 'code'}
|
|
||||||
<polyline points="16 18 22 12 16 6" />
|
|
||||||
<polyline points="8 6 2 12 8 18" />
|
|
||||||
{:else if name === 'copy'}
|
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
|
||||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
||||||
{:else if name === 'settings'}
|
|
||||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
|
||||||
<circle cx="12" cy="12" r="3" />
|
|
||||||
{:else if name === 'chevron-down'}
|
|
||||||
<polyline points="6 9 12 15 18 9" />
|
|
||||||
{:else if name === 'chevron-right'}
|
|
||||||
<polyline points="9 18 15 12 9 6" />
|
|
||||||
{:else if name === 'chevron-up'}
|
|
||||||
<polyline points="18 15 12 9 6 15" />
|
|
||||||
{:else if name === 'activity'}
|
|
||||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
|
||||||
{:else if name === 'shield'}
|
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
||||||
{:else if name === 'git-branch'}
|
|
||||||
<line x1="6" y1="3" x2="6" y2="15" />
|
|
||||||
<circle cx="18" cy="6" r="3" />
|
|
||||||
<circle cx="6" cy="18" r="3" />
|
|
||||||
<path d="M18 9a9 9 0 0 1-9 9" />
|
|
||||||
{:else if name === 'clock'}
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<polyline points="12 6 12 12 16 14" />
|
|
||||||
{:else if name === 'layers'}
|
|
||||||
<polygon points="12 2 2 7 12 12 22 7 12 2" />
|
|
||||||
<polyline points="2 17 12 22 22 17" />
|
|
||||||
<polyline points="2 12 12 17 22 12" />
|
|
||||||
{:else if name === 'cpu'}
|
|
||||||
<rect width="16" height="16" x="4" y="4" rx="2" />
|
|
||||||
<rect width="6" height="6" x="9" y="9" rx="1" />
|
|
||||||
<path d="M15 2v2" /><path d="M15 20v2" /><path d="M2 15h2" /><path d="M2 9h2" />
|
|
||||||
<path d="M20 15h2" /><path d="M20 9h2" /><path d="M9 2v2" /><path d="M9 20v2" />
|
|
||||||
{:else if name === 'zap'}
|
|
||||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
|
||||||
{:else if name === 'crosshair'}
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<line x1="22" y1="12" x2="18" y2="12" />
|
|
||||||
<line x1="6" y1="12" x2="2" y2="12" />
|
|
||||||
<line x1="12" y1="6" x2="12" y2="2" />
|
|
||||||
<line x1="12" y1="22" x2="12" y2="18" />
|
|
||||||
{:else if name === 'target'}
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<circle cx="12" cy="12" r="6" />
|
|
||||||
<circle cx="12" cy="12" r="2" />
|
|
||||||
{:else if name === 'trending-down'}
|
|
||||||
<polyline points="23 18 13.5 8.5 8.5 13.5 1 6" />
|
|
||||||
<polyline points="17 18 23 18 23 12" />
|
|
||||||
{:else if name === 'book-open'}
|
|
||||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
|
|
||||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
|
|
||||||
{:else if name === 'file-text'}
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
<line x1="16" y1="13" x2="8" y2="13" />
|
|
||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
|
||||||
{:else if name === 'plus'}
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19" />
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
|
||||||
{:else if name === 'trash'}
|
|
||||||
<polyline points="3 6 5 6 21 6" />
|
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
||||||
{:else if name === 'close'}
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
{/if}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.svg-icon {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,747 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { ScoringRunResponse } from '../api/types';
|
|
||||||
import { consoleState } from '../state/consoleState.svelte';
|
|
||||||
import Icon from './Icon.svelte';
|
|
||||||
import RunStatusBadge from './RunStatusBadge.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
run: ScoringRunResponse | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { run }: Props = $props();
|
|
||||||
let showRawJson = $state(false);
|
|
||||||
let copiedRunId = $state(false);
|
|
||||||
let copiedJson = $state(false);
|
|
||||||
|
|
||||||
async function copyRunId() {
|
|
||||||
if (!run?.run_id) return;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(run.run_id);
|
|
||||||
copiedRunId = true;
|
|
||||||
setTimeout(() => (copiedRunId = false), 2000);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyJson() {
|
|
||||||
if (!run) return;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(JSON.stringify(run, null, 2));
|
|
||||||
copiedJson = true;
|
|
||||||
setTimeout(() => (copiedJson = false), 2000);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateDuration(startStr?: string | null, endStr?: string | null): string | null {
|
|
||||||
if (!startStr || !endStr) return null;
|
|
||||||
try {
|
|
||||||
const start = new Date(startStr).getTime();
|
|
||||||
const end = new Date(endStr).getTime();
|
|
||||||
if (isNaN(start) || isNaN(end)) return null;
|
|
||||||
const diffMs = end - start;
|
|
||||||
if (diffMs < 1000) return `${diffMs}ms`;
|
|
||||||
return `${(diffMs / 1000).toFixed(2)}s`;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let duration = $derived(calculateDuration(run?.started_at, run?.completed_at));
|
|
||||||
let currentFixture = $derived(consoleState.selectedFixture);
|
|
||||||
|
|
||||||
// Default lead scoring hero image
|
|
||||||
let defaultHeroImage = '/images/lead-ielts-student.jpg';
|
|
||||||
let activeHeroImage = $derived(currentFixture?.coverImage ?? defaultHeroImage);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="inspector-card">
|
|
||||||
<!-- Top Inspector Header -->
|
|
||||||
<div class="inspector-header">
|
|
||||||
<div class="title-row">
|
|
||||||
<div class="icon-bubble">
|
|
||||||
<Icon name="activity" size={17} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="card-title">Chi tiết tiến trình đánh giá</h3>
|
|
||||||
<span class="card-subtitle">Trạng thái thực thi, máy trạng thái và dòng thời gian vòng đời</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-actions">
|
|
||||||
{#if run}
|
|
||||||
<RunStatusBadge status={run.status} degraded={run.degraded} />
|
|
||||||
<button
|
|
||||||
class="btn-refresh {consoleState.isLoading ? 'spinning' : ''}"
|
|
||||||
disabled={consoleState.isLoading}
|
|
||||||
onclick={() => consoleState.refreshRun()}
|
|
||||||
title="Làm mới trạng thái từ backend"
|
|
||||||
>
|
|
||||||
<Icon name="refresh" size={13} class="refresh-icon" />
|
|
||||||
<span>Làm mới</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Editorial Visual Hero Showcase (SiteInspire Style Anchor) -->
|
|
||||||
<div class="editorial-hero-frame">
|
|
||||||
<div class="hero-image-wrap">
|
|
||||||
<img src={activeHeroImage} alt="Kịch bản đánh giá lead" class="hero-image" />
|
|
||||||
<div class="hero-scrim"></div>
|
|
||||||
|
|
||||||
<div class="hero-meta-overlay">
|
|
||||||
<div class="hero-meta-top">
|
|
||||||
<span class="hero-category font-mono">{currentFixture.category ?? 'ĐÁNH GIÁ LEAD'}</span>
|
|
||||||
{#if run}
|
|
||||||
<span class="hero-status-tag font-mono">{run.status.toUpperCase()}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hero-meta-bot">
|
|
||||||
<h2 class="hero-headline">{run ? `Mã Lead: ${run.subject_ref}` : currentFixture.name}</h2>
|
|
||||||
<p class="hero-subtext">
|
|
||||||
{run
|
|
||||||
? `Tenant: ${run.tenant_id} · Phiên bản cấu hình: ${run.scoring_config_version}`
|
|
||||||
: currentFixture.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if !run}
|
|
||||||
<!-- Empty State Guide -->
|
|
||||||
<div class="empty-guide-bar">
|
|
||||||
<div class="empty-guide-left">
|
|
||||||
<Icon name="crosshair" size={18} class="text-secondary" />
|
|
||||||
<div class="empty-guide-text">
|
|
||||||
<strong>Sẵn sàng chạy kiểm thử</strong>
|
|
||||||
<span>Chọn một kịch bản kiểm thử ở cột bên trái và nhấn <strong>Chạy đánh giá điểm lead</strong> để kiểm tra.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!-- Run Identifier Bar -->
|
|
||||||
<div class="id-banner">
|
|
||||||
<div class="id-cluster">
|
|
||||||
<span class="id-tag">MÃ PHIÊN CHẠY:</span>
|
|
||||||
<code class="id-val font-mono">{run.run_id}</code>
|
|
||||||
</div>
|
|
||||||
<button class="btn-copy-id" onclick={copyRunId} title="Sao chép Run ID">
|
|
||||||
<Icon name={copiedRunId ? 'check' : 'copy'} size={12} />
|
|
||||||
<span>{copiedRunId ? 'Đã chép' : 'Chép mã'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lineage Link if parent exists -->
|
|
||||||
{#if run.parent_run_id}
|
|
||||||
<div class="lineage-banner">
|
|
||||||
<Icon name="git-branch" size={14} class="text-indigo" />
|
|
||||||
<div class="lineage-text">
|
|
||||||
<span class="lineage-label">Nguồn gốc phiên bản cha:</span>
|
|
||||||
<code class="lineage-id font-mono">{run.parent_run_id}</code>
|
|
||||||
</div>
|
|
||||||
<span class="lineage-tag font-mono">v{run.version - 1} → v{run.version}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Key Details Grid -->
|
|
||||||
<div class="details-grid">
|
|
||||||
<div class="detail-cell">
|
|
||||||
<span class="cell-label">Mã Tenant</span>
|
|
||||||
<span class="cell-val font-mono">{run.tenant_id}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-cell">
|
|
||||||
<span class="cell-label">Tham chiếu Lead</span>
|
|
||||||
<span class="cell-val font-mono">{run.subject_ref}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-cell">
|
|
||||||
<span class="cell-label">Phiên bản cấu hình</span>
|
|
||||||
<span class="cell-val font-mono">{run.scoring_config_version}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-cell">
|
|
||||||
<span class="cell-label">Số hiệu phiên bản</span>
|
|
||||||
<span class="cell-val font-mono">v{run.version}</span>
|
|
||||||
</div>
|
|
||||||
{#if duration}
|
|
||||||
<div class="detail-cell highlight">
|
|
||||||
<span class="cell-label">Tổng thời gian thực thi</span>
|
|
||||||
<span class="cell-val font-mono text-cyan">{duration}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lifecycle Timeline -->
|
|
||||||
<div class="timeline-container">
|
|
||||||
<div class="timeline-head">
|
|
||||||
<Icon name="clock" size={13} class="text-secondary" />
|
|
||||||
<span class="timeline-title">Dấu thời gian vòng đời thực thi</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timeline-track">
|
|
||||||
<div class="timeline-step">
|
|
||||||
<span class="step-point created"></span>
|
|
||||||
<div class="step-data">
|
|
||||||
<span class="step-name">Khởi tạo</span>
|
|
||||||
<code class="step-timestamp font-mono">{run.created_at}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if run.started_at}
|
|
||||||
<div class="step-line"></div>
|
|
||||||
<div class="timeline-step">
|
|
||||||
<span class="step-point started"></span>
|
|
||||||
<div class="step-data">
|
|
||||||
<span class="step-name">Đang xử lý</span>
|
|
||||||
<code class="step-timestamp font-mono">{run.started_at}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if run.completed_at}
|
|
||||||
<div class="step-line"></div>
|
|
||||||
<div class="timeline-step">
|
|
||||||
<span class="step-point completed"></span>
|
|
||||||
<div class="step-data">
|
|
||||||
<span class="step-name">Hoàn thành</span>
|
|
||||||
<code class="step-timestamp font-mono">{run.completed_at}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Raw JSON Inspector -->
|
|
||||||
<div class="raw-drawer">
|
|
||||||
<div class="raw-top">
|
|
||||||
<button
|
|
||||||
class="btn-raw-toggle"
|
|
||||||
onclick={() => (showRawJson = !showRawJson)}
|
|
||||||
aria-expanded={showRawJson}
|
|
||||||
>
|
|
||||||
<Icon name={showRawJson ? 'chevron-down' : 'chevron-right'} size={14} />
|
|
||||||
<span>{showRawJson ? 'Ẩn dữ liệu JSON đầy đủ' : 'Xem toàn bộ bản ghi JSON'}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if showRawJson}
|
|
||||||
<button class="btn-copy-raw" onclick={copyJson}>
|
|
||||||
<Icon name={copiedJson ? 'check' : 'copy'} size={12} />
|
|
||||||
<span>{copiedJson ? 'Đã chép' : 'Chép JSON'}</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showRawJson}
|
|
||||||
<pre class="raw-json-code font-mono">{JSON.stringify(run, null, 2)}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.inspector-card {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 20px 24px;
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inspector-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
padding-bottom: 14px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-bubble {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--accent-cyan-subtle);
|
|
||||||
border: 1px solid var(--status-info-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.02rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-subtitle {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-refresh {
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 5px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.76rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-refresh:hover:not(:disabled) {
|
|
||||||
border-color: var(--border-medium);
|
|
||||||
background: var(--bg-surface-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-refresh:disabled {
|
|
||||||
opacity: 0.55;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-refresh.spinning :global(.refresh-icon) {
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Editorial Hero Showcase (SiteInspire Style Frame) */
|
|
||||||
.editorial-hero-frame {
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 6px;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-image-wrap {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 220px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
overflow: hidden;
|
|
||||||
background: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
.hero-image-wrap {
|
|
||||||
height: 180px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
transition: transform 0.6s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-image-wrap:hover .hero-image {
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-scrim {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.25) 0%, rgba(15, 23, 42, 0.85) 100%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-meta-overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
padding: 16px 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: #ffffff;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-meta-top {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-category {
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
padding: 3px 10px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-status-tag {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
background: #0284c7;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-meta-bot {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-headline {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #ffffff;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtext {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
max-width: 600px;
|
|
||||||
line-height: 1.4;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-guide-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-guide-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-guide-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-guide-text strong {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-guide-text span {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.id-banner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 10px 14px;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.id-cluster {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.id-tag {
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.id-val {
|
|
||||||
font-size: 0.84rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy-id {
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy-id:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--border-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lineage-banner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
background: var(--accent-indigo-subtle);
|
|
||||||
border: 1px solid rgba(79, 70, 229, 0.2);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 8px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lineage-banner :global(.text-indigo) {
|
|
||||||
color: var(--accent-indigo);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lineage-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1px;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lineage-label {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lineage-id {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--accent-indigo);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lineage-tag {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 2px 8px;
|
|
||||||
background: rgba(79, 70, 229, 0.12);
|
|
||||||
color: var(--accent-indigo);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-cell {
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-cell.highlight {
|
|
||||||
border-color: var(--status-info-border);
|
|
||||||
background: var(--accent-cyan-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-label {
|
|
||||||
font-size: 0.66rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-val {
|
|
||||||
font-size: 0.84rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-cyan {
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-container {
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 12px 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-title {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-track {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-point {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-point.created { background: var(--text-muted); }
|
|
||||||
.step-point.started { background: var(--accent-cyan); }
|
|
||||||
.step-point.completed { background: var(--status-good); }
|
|
||||||
|
|
||||||
.step-data {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-name {
|
|
||||||
font-size: 0.62rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-timestamp {
|
|
||||||
font-size: 0.74rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-line {
|
|
||||||
width: 18px;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--border-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.raw-drawer {
|
|
||||||
border-top: 1px solid var(--border-subtle);
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.raw-top {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-raw-toggle {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 0;
|
|
||||||
transition: color var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-raw-toggle:hover {
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy-raw {
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy-raw:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--border-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.raw-json-code {
|
|
||||||
margin-top: 10px;
|
|
||||||
background: #0f172a;
|
|
||||||
border: 1px solid #1e293b;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 12px 14px;
|
|
||||||
font-size: 0.74rem;
|
|
||||||
color: #93c5fd;
|
|
||||||
overflow-x: auto;
|
|
||||||
max-height: 240px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { ScoringRunStatus } from '../api/types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
status: ScoringRunStatus | string;
|
|
||||||
degraded?: boolean | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { status, degraded = false }: Props = $props();
|
|
||||||
|
|
||||||
const statusConfig: Record<string, { bg: string; text: string; border: string; label: string; pulse?: boolean }> = {
|
|
||||||
pending: {
|
|
||||||
bg: 'var(--status-warning-bg)',
|
|
||||||
text: 'var(--status-warning-text)',
|
|
||||||
border: 'var(--status-warning-border)',
|
|
||||||
label: 'ĐANG CHỜ',
|
|
||||||
pulse: true,
|
|
||||||
},
|
|
||||||
running: {
|
|
||||||
bg: 'var(--status-info-bg)',
|
|
||||||
text: 'var(--status-info)',
|
|
||||||
border: 'var(--status-info-border)',
|
|
||||||
label: 'ĐANG XỬ LÝ',
|
|
||||||
pulse: true,
|
|
||||||
},
|
|
||||||
completed: {
|
|
||||||
bg: 'var(--status-good-bg)',
|
|
||||||
text: 'var(--status-good-text)',
|
|
||||||
border: 'var(--status-good-border)',
|
|
||||||
label: 'HOÀN THÀNH',
|
|
||||||
},
|
|
||||||
review_required: {
|
|
||||||
bg: 'var(--status-warning-bg)',
|
|
||||||
text: 'var(--status-warning-text)',
|
|
||||||
border: 'var(--status-warning-border)',
|
|
||||||
label: 'CẦN KIỂM TRA',
|
|
||||||
},
|
|
||||||
failed: {
|
|
||||||
bg: 'var(--status-critical-bg)',
|
|
||||||
text: 'var(--status-critical-text)',
|
|
||||||
border: 'var(--status-critical-border)',
|
|
||||||
label: 'THẤT BẠI',
|
|
||||||
},
|
|
||||||
superseded: {
|
|
||||||
bg: 'var(--bg-surface-raised)',
|
|
||||||
text: 'var(--text-muted)',
|
|
||||||
border: 'var(--border-subtle)',
|
|
||||||
label: 'ĐÃ THAY THẾ',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let badge = $derived(
|
|
||||||
statusConfig[status] ?? {
|
|
||||||
bg: 'var(--bg-surface-raised)',
|
|
||||||
text: 'var(--text-secondary)',
|
|
||||||
border: 'var(--border-subtle)',
|
|
||||||
label: status.toUpperCase(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="status-wrapper">
|
|
||||||
<span
|
|
||||||
class="badge"
|
|
||||||
style="background: {badge.bg}; color: {badge.text}; border: 1px solid {badge.border};"
|
|
||||||
>
|
|
||||||
<span class="dot {badge.pulse ? 'pulsing' : ''}" style="background: {badge.text};"></span>
|
|
||||||
<span>{badge.label}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{#if degraded}
|
|
||||||
<span class="degraded-badge" title="Chế độ dự phòng: AIHub gặp sự cố, hệ thống chuyển về tính điểm quy tắc">
|
|
||||||
DỰ PHÒNG
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.status-wrapper {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 3px 9px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot.pulsing {
|
|
||||||
animation: pulse-glow 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
|
||||||
0%, 100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.3);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.degraded-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
background: var(--status-warning-bg);
|
|
||||||
color: var(--status-warning-text);
|
|
||||||
border: 1px solid var(--status-warning-border);
|
|
||||||
font-size: 0.66rem;
|
|
||||||
font-weight: 700;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,587 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { ScoringRunResponse } from '../api/types';
|
|
||||||
import Icon from './Icon.svelte';
|
|
||||||
import ScoreMeter from './ScoreMeter.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
run: ScoringRunResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { run }: Props = $props();
|
|
||||||
|
|
||||||
let effectiveScore = $derived(run.effective_score ?? run.final_score ?? run.rule_score ?? null);
|
|
||||||
let ruleScore = $derived(run.rule_score ?? null);
|
|
||||||
let aiScore = $derived(run.ai_score ?? null);
|
|
||||||
|
|
||||||
let confidencePercent = $derived(run.confidence !== null ? Math.round(run.confidence * 100) : null);
|
|
||||||
let thresholdPercent = $derived(
|
|
||||||
run.confidence_threshold !== null ? Math.round(run.confidence_threshold * 100) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
let confidenceMet = $derived(
|
|
||||||
run.confidence !== null && run.confidence_threshold !== null
|
|
||||||
? run.confidence >= run.confidence_threshold
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
let showMetadata = $state(false);
|
|
||||||
let copiedMeta = $state(false);
|
|
||||||
|
|
||||||
async function copyMetadata() {
|
|
||||||
if (!run.ai_metadata) return;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(JSON.stringify(run.ai_metadata, null, 2));
|
|
||||||
copiedMeta = true;
|
|
||||||
setTimeout(() => (copiedMeta = false), 2000);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="score-card">
|
|
||||||
<!-- Card Header -->
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<div class="icon-bubble">
|
|
||||||
<Icon name="layers" size={17} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="card-title">Phân tích điểm chi tiết</h3>
|
|
||||||
<span class="card-subtitle">Chỉ số đánh giá từ bộ quy tắc xác định và mô hình AIHub</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-badges">
|
|
||||||
{#if run.scoring_mode}
|
|
||||||
<span class="mode-badge">
|
|
||||||
<span class="mode-dot"></span>
|
|
||||||
<span>{run.scoring_mode.toUpperCase()}</span>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{#if run.version > 1}
|
|
||||||
<span class="version-badge font-mono">
|
|
||||||
PHIÊN BẢN v{run.version}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hero Score Row: Radial Meter + Metric Breakdown Bars -->
|
|
||||||
<div class="hero-score-layout">
|
|
||||||
<!-- Left: Score Ring Meter -->
|
|
||||||
<div class="meter-panel">
|
|
||||||
<ScoreMeter score={effectiveScore} size={155} label="Điểm hiệu lực" />
|
|
||||||
{#if run.effective_score !== null && run.final_score !== null && run.effective_score !== run.final_score}
|
|
||||||
<div class="override-notice font-mono">
|
|
||||||
<Icon name="sliders" size={12} />
|
|
||||||
<span>Đã hiệu chỉnh từ {run.final_score}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right: Detailed Metric Bars -->
|
|
||||||
<div class="bars-panel">
|
|
||||||
<!-- Rule Engine Score Bar -->
|
|
||||||
<div class="bar-item">
|
|
||||||
<div class="bar-header">
|
|
||||||
<div class="bar-title">
|
|
||||||
<Icon name="cpu" size={14} class="text-secondary" />
|
|
||||||
<span>Điểm quy tắc xác định</span>
|
|
||||||
</div>
|
|
||||||
<span class="bar-value font-mono">{ruleScore !== null ? `${ruleScore}/100` : '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="progress-track">
|
|
||||||
<div
|
|
||||||
class="progress-fill rule-gradient"
|
|
||||||
style="width: {ruleScore !== null ? `${ruleScore}%` : '0%'};"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="bar-footer">
|
|
||||||
<span>Kiểm tra điều kiện biên và quy tắc chấm điểm cứng</span>
|
|
||||||
<span class="font-mono text-muted">{ruleScore !== null ? (ruleScore >= 70 ? 'Đạt cao' : ruleScore >= 40 ? 'Đạt TB' : 'Thấp') : 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- AI LLM Score Bar -->
|
|
||||||
<div class="bar-item">
|
|
||||||
<div class="bar-header">
|
|
||||||
<div class="bar-title">
|
|
||||||
<Icon name="sparkles" size={14} class="text-secondary" />
|
|
||||||
<span>Điểm đánh giá mô hình AI</span>
|
|
||||||
</div>
|
|
||||||
<span class="bar-value font-mono">{aiScore !== null ? `${aiScore}/100` : '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="progress-track">
|
|
||||||
<div
|
|
||||||
class="progress-fill ai-gradient"
|
|
||||||
style="width: {aiScore !== null ? `${aiScore}%` : '0%'};"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="bar-footer">
|
|
||||||
<span>Đánh giá ngữ nghĩa hội thoại và ý định của lead</span>
|
|
||||||
<span class="font-mono text-muted">{aiScore !== null ? 'Đã đánh giá' : run.degraded ? 'Dự phòng quy tắc' : 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- AI Confidence vs Threshold Bar -->
|
|
||||||
<div class="bar-item {confidenceMet === false ? 'warn-border' : ''}">
|
|
||||||
<div class="bar-header">
|
|
||||||
<div class="bar-title">
|
|
||||||
<Icon name="crosshair" size={14} class="text-secondary" />
|
|
||||||
<span>Độ tin cậy mô hình & Ngưỡng kiểm soát</span>
|
|
||||||
</div>
|
|
||||||
<div class="bar-value font-mono">
|
|
||||||
<span>{confidencePercent !== null ? `${confidencePercent}%` : '-'}</span>
|
|
||||||
{#if thresholdPercent !== null}
|
|
||||||
<span class="threshold-tag">Yêu cầu: {thresholdPercent}%</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="progress-track double-track">
|
|
||||||
<!-- Confidence fill -->
|
|
||||||
<div
|
|
||||||
class="progress-fill conf-gradient {confidenceMet === false ? 'below-gate' : ''}"
|
|
||||||
style="width: {confidencePercent !== null ? `${confidencePercent}%` : '0%'};"
|
|
||||||
></div>
|
|
||||||
<!-- Threshold marker line -->
|
|
||||||
{#if thresholdPercent !== null}
|
|
||||||
<div
|
|
||||||
class="threshold-marker"
|
|
||||||
style="left: {thresholdPercent}%;"
|
|
||||||
title="Ngưỡng kiểm soát chất lượng: {thresholdPercent}%"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bar-footer">
|
|
||||||
<span>
|
|
||||||
{#if confidenceMet === true}
|
|
||||||
<span class="status-pass font-mono">✓ Đạt ngưỡng kiểm soát ({confidencePercent}% ≥ {thresholdPercent}%)</span>
|
|
||||||
{:else if confidenceMet === false}
|
|
||||||
<span class="status-warn font-mono">⚠ Dưới ngưỡng kiểm soát ({confidencePercent}% < {thresholdPercent}%)</span>
|
|
||||||
{:else}
|
|
||||||
<span>Chưa kích hoạt đánh giá độ tin cậy</span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{#if thresholdPercent !== null}
|
|
||||||
<span class="font-mono text-muted">Ngưỡng: {thresholdPercent}%</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fault / Error Banner -->
|
|
||||||
{#if run.error_code}
|
|
||||||
<div class="fault-banner">
|
|
||||||
<div class="fault-content">
|
|
||||||
<Icon name="alert-circle" size={18} class="text-critical" />
|
|
||||||
<div>
|
|
||||||
<span class="fault-title">Sự cố thực thi: <code class="font-mono">{run.error_code}</code></span>
|
|
||||||
<p class="fault-msg">Tiến trình xử lý bị gián đoạn hoặc gặp lỗi từ máy chủ upstream.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if run.retryable !== null}
|
|
||||||
<span class="retry-badge font-mono {run.retryable ? 'retry-yes' : 'retry-no'}">
|
|
||||||
{run.retryable ? 'TỰ ĐỘNG THỬ LẠI' : 'KHÔNG THỬ LẠI'}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- AI Execution Metadata Section -->
|
|
||||||
{#if run.ai_metadata}
|
|
||||||
<div class="meta-wrapper">
|
|
||||||
<div class="meta-toolbar">
|
|
||||||
<button
|
|
||||||
class="btn-meta-toggle"
|
|
||||||
onclick={() => (showMetadata = !showMetadata)}
|
|
||||||
aria-expanded={showMetadata}
|
|
||||||
>
|
|
||||||
<Icon name={showMetadata ? 'chevron-down' : 'chevron-right'} size={14} />
|
|
||||||
<span>Dữ liệu thực thi AI & Lượng token sử dụng ({Object.keys(run.ai_metadata).length} tham số)</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if showMetadata}
|
|
||||||
<button class="btn-copy-code" onclick={copyMetadata}>
|
|
||||||
<Icon name={copiedMeta ? 'check' : 'copy'} size={13} />
|
|
||||||
<span>{copiedMeta ? 'Đã chép' : 'Chép JSON'}</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showMetadata}
|
|
||||||
<pre class="meta-code font-mono">{JSON.stringify(run.ai_metadata, null, 2)}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.score-card {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 20px 24px;
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
padding-bottom: 14px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-bubble {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--accent-cyan-subtle);
|
|
||||||
border: 1px solid var(--status-info-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.02rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-subtitle {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-badges {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
background: var(--accent-cyan-subtle);
|
|
||||||
border: 1px solid var(--status-info-border);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-badge {
|
|
||||||
padding: 3px 8px;
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-score-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 180px 1fr;
|
|
||||||
gap: 24px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
|
||||||
.hero-score-layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-panel {
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 16px 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.override-notice {
|
|
||||||
font-size: 0.68rem;
|
|
||||||
color: var(--status-warning);
|
|
||||||
background: var(--status-warning-bg);
|
|
||||||
border: 1px solid var(--status-warning-border);
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bars-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-item {
|
|
||||||
background: var(--bg-surface-subtle);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 10px 14px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-item:hover {
|
|
||||||
border-color: var(--border-medium);
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-item.warn-border {
|
|
||||||
border-color: var(--status-warning-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-value {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.threshold-tag {
|
|
||||||
font-size: 0.68rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track {
|
|
||||||
height: 6px;
|
|
||||||
background: rgba(15, 23, 42, 0.07);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track.double-track {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
transition: width 0.6s var(--transition-spring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-gradient {
|
|
||||||
background: linear-gradient(90deg, #0284c7, #38bdf8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-gradient {
|
|
||||||
background: linear-gradient(90deg, #4f46e5, #818cf8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.conf-gradient {
|
|
||||||
background: linear-gradient(90deg, #059669, #10b981);
|
|
||||||
}
|
|
||||||
|
|
||||||
.conf-gradient.below-gate {
|
|
||||||
background: linear-gradient(90deg, #d97706, #f59e0b);
|
|
||||||
}
|
|
||||||
|
|
||||||
.threshold-marker {
|
|
||||||
position: absolute;
|
|
||||||
top: -3px;
|
|
||||||
bottom: -3px;
|
|
||||||
width: 2px;
|
|
||||||
background: var(--status-critical);
|
|
||||||
box-shadow: 0 0 4px var(--status-critical);
|
|
||||||
border-radius: 1px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pass {
|
|
||||||
color: var(--status-good);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-warn {
|
|
||||||
color: var(--status-warning);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fault-banner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--status-critical-bg);
|
|
||||||
border: 1px solid var(--status-critical-border);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fault-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fault-title {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--status-critical);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fault-title code {
|
|
||||||
background: #ffffff;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--status-critical-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fault-msg {
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-badge {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-badge.retry-yes {
|
|
||||||
background: var(--status-good-bg);
|
|
||||||
color: var(--status-good);
|
|
||||||
border: 1px solid var(--status-good-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-badge.retry-no {
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
color: var(--text-muted);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-wrapper {
|
|
||||||
border-top: 1px solid var(--border-subtle);
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-meta-toggle {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 0;
|
|
||||||
transition: color var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-meta-toggle:hover {
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy-code {
|
|
||||||
background: var(--bg-surface-raised);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-copy-code:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--border-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-code {
|
|
||||||
margin-top: 10px;
|
|
||||||
background: #0f172a;
|
|
||||||
border: 1px solid #1e293b;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 12px 14px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #93c5fd;
|
|
||||||
overflow-x: auto;
|
|
||||||
max-height: 200px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,195 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
score: number | null;
|
|
||||||
size?: number;
|
|
||||||
strokeWidth?: number;
|
|
||||||
label?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { score, size = 160, strokeWidth = 10, label = 'Điểm hiệu lực' }: Props = $props();
|
|
||||||
|
|
||||||
let radius = $derived((size - strokeWidth - 12) / 2);
|
|
||||||
let circumference = $derived(2 * Math.PI * radius);
|
|
||||||
let validScore = $derived(score !== null ? Math.max(0, Math.min(100, score)) : 0);
|
|
||||||
let dashOffset = $derived(
|
|
||||||
score !== null ? circumference - (validScore / 100) * circumference : circumference
|
|
||||||
);
|
|
||||||
|
|
||||||
let scoreTier = $derived.by(() => {
|
|
||||||
if (score === null) {
|
|
||||||
return {
|
|
||||||
name: 'CHƯA CÓ ĐIỂM',
|
|
||||||
color: 'var(--text-muted)',
|
|
||||||
bg: 'var(--bg-surface-raised)',
|
|
||||||
border: 'var(--border-subtle)',
|
|
||||||
gradient: ['#94a3b8', '#64748b'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (score >= 70) {
|
|
||||||
return {
|
|
||||||
name: 'TIỀM NĂNG CAO',
|
|
||||||
color: 'var(--status-good)',
|
|
||||||
bg: 'var(--status-good-bg)',
|
|
||||||
border: 'var(--status-good-border)',
|
|
||||||
gradient: ['#059669', '#10b981'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (score >= 40) {
|
|
||||||
return {
|
|
||||||
name: 'TRUNG BÌNH',
|
|
||||||
color: 'var(--status-warning)',
|
|
||||||
bg: 'var(--status-warning-bg)',
|
|
||||||
border: 'var(--status-warning-border)',
|
|
||||||
gradient: ['#d97706', '#f59e0b'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: 'TIỀM NĂNG THẤP',
|
|
||||||
color: 'var(--status-critical)',
|
|
||||||
bg: 'var(--status-critical-bg)',
|
|
||||||
border: 'var(--status-critical-border)',
|
|
||||||
gradient: ['#e11d48', '#f43f5e'],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="meter-wrapper" style="--size: {size}px;">
|
|
||||||
<svg class="meter-svg" width={size} height={size} viewBox="0 0 {size} {size}">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="meter-grad-{size}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" stop-color={scoreTier.gradient[0]} />
|
|
||||||
<stop offset="100%" stop-color={scoreTier.gradient[1]} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Outer Track -->
|
|
||||||
<circle
|
|
||||||
class="meter-outer-track"
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius + strokeWidth / 2 + 4}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Background Base Ring -->
|
|
||||||
<circle
|
|
||||||
class="meter-track"
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke-width={strokeWidth}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Value Progress Ring -->
|
|
||||||
{#if score !== null}
|
|
||||||
<circle
|
|
||||||
class="meter-value"
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke="url(#meter-grad-{size})"
|
|
||||||
stroke-width={strokeWidth}
|
|
||||||
stroke-dasharray={circumference}
|
|
||||||
stroke-dashoffset={dashOffset}
|
|
||||||
stroke-linecap="round"
|
|
||||||
transform="rotate(-90 {size / 2} {size / 2})"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Center Metric Data -->
|
|
||||||
<div class="meter-core">
|
|
||||||
<span class="meter-label">{label}</span>
|
|
||||||
<div class="meter-number font-mono" style="color: {score !== null ? scoreTier.color : 'var(--text-muted)'};">
|
|
||||||
{score !== null ? validScore : '-'}
|
|
||||||
<span class="meter-max">/100</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="tier-pill font-mono"
|
|
||||||
style="color: {scoreTier.color}; background: {scoreTier.bg}; border-color: {scoreTier.border};"
|
|
||||||
>
|
|
||||||
{scoreTier.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.meter-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-svg {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-outer-track {
|
|
||||||
fill: none;
|
|
||||||
stroke: rgba(15, 23, 42, 0.06);
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 3 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-track {
|
|
||||||
fill: none;
|
|
||||||
stroke: rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-value {
|
|
||||||
fill: none;
|
|
||||||
transition: stroke-dashoffset 0.7s var(--transition-spring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-core {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
gap: 2px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-label {
|
|
||||||
font-size: 0.62rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-number {
|
|
||||||
font-size: 2.3rem;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2px;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meter-max {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-muted);
|
|
||||||
letter-spacing: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tier-pill {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
margin-top: 4px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,238 +0,0 @@
|
|||||||
import type { GoldenFixture } from '../api/types';
|
|
||||||
|
|
||||||
export const GOLDEN_FIXTURES: GoldenFixture[] = [
|
|
||||||
{
|
|
||||||
id: 'happy-path-medium-ai',
|
|
||||||
name: '1. Luồng chuẩn (AI 2 bước hoàn chỉnh)',
|
|
||||||
category: 'Lớp học IELTS cấp tốc',
|
|
||||||
coverImage: '/images/lead-ielts-student.jpg',
|
|
||||||
description: 'Hồ sơ lead đầy đủ kèm lịch sử trao đổi chi tiết. Kỳ vọng đạt trạng thái completed với điểm kết hợp Quy tắc và AI.',
|
|
||||||
expectedStatus: 'completed',
|
|
||||||
expectedMode: 'medium_ai',
|
|
||||||
payload: {
|
|
||||||
tenant_id: 'omni-test-tenant-42',
|
|
||||||
subject_ref: 'ems-lead-dev-1001',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
snapshot: {
|
|
||||||
version: '1.0.0',
|
|
||||||
scope: 'linked_contact_history',
|
|
||||||
captured_at: '2026-08-28T12:00:00Z',
|
|
||||||
lead_profile: {
|
|
||||||
ems_lead_id: 'ems-lead-dev-1001',
|
|
||||||
name: 'Nguyen Van An',
|
|
||||||
phone: '+84901234567',
|
|
||||||
email: 'an.nguyen@example.com',
|
|
||||||
status: 'new',
|
|
||||||
},
|
|
||||||
conversations: [
|
|
||||||
{
|
|
||||||
conversation_id: 'conv-zalo-101',
|
|
||||||
channel: 'zalo',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
message_id: 'msg-01',
|
|
||||||
sender_type: 'contact',
|
|
||||||
sent_at: '2026-08-28T11:01:00Z',
|
|
||||||
content: 'Xin chao, toi muon tim hieu khoa hoc IELTS cap toc.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message_id: 'msg-02',
|
|
||||||
sender_type: 'agent',
|
|
||||||
sent_at: '2026-08-28T11:02:00Z',
|
|
||||||
content: 'Chao anh An, ben em co lop IELTS Intensive target 7.0+ khai giang tuan sau.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message_id: 'msg-03',
|
|
||||||
sender_type: 'contact',
|
|
||||||
sent_at: '2026-08-28T11:03:00Z',
|
|
||||||
content: 'Hoc phi bao nhieu va hoc online hay offline vay ban?',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hitl_policy: {
|
|
||||||
policy_version: '1.0.0',
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'review-required-low-confidence',
|
|
||||||
name: '2. Cần duyệt thủ công (Hội thoại ngắn)',
|
|
||||||
category: 'Tư vấn tuyển sinh',
|
|
||||||
coverImage: '/images/lead-counseling-review.jpg',
|
|
||||||
description: 'Lịch sử tin nhắn ngắn qua mạng xã hội. Kỳ vọng độ tin cậy thấp, kích hoạt quy trình người duyệt xếp lớp.',
|
|
||||||
expectedStatus: 'completed',
|
|
||||||
expectedMode: 'medium_ai',
|
|
||||||
payload: {
|
|
||||||
tenant_id: 'omni-test-tenant-42',
|
|
||||||
subject_ref: 'ems-lead-dev-1002',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
snapshot: {
|
|
||||||
version: '1.0.0',
|
|
||||||
scope: 'linked_contact_history',
|
|
||||||
captured_at: '2026-08-28T12:05:00Z',
|
|
||||||
lead_profile: {
|
|
||||||
ems_lead_id: 'ems-lead-dev-1002',
|
|
||||||
name: 'Tran Thi B',
|
|
||||||
phone: '+84912345678',
|
|
||||||
email: null,
|
|
||||||
status: 'contacted',
|
|
||||||
},
|
|
||||||
conversations: [
|
|
||||||
{
|
|
||||||
conversation_id: 'conv-fb-201',
|
|
||||||
channel: 'facebook',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
message_id: 'msg-04',
|
|
||||||
sender_type: 'contact',
|
|
||||||
sent_at: '2026-08-28T12:01:00Z',
|
|
||||||
content: 'Hi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hitl_policy: {
|
|
||||||
policy_version: '1.0.0',
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'degraded-rules-fallback',
|
|
||||||
name: '3. Chế độ dự phòng (Chỉ tính điểm quy tắc)',
|
|
||||||
category: 'Đào tạo doanh nghiệp',
|
|
||||||
coverImage: '/images/lead-enterprise-training.jpg',
|
|
||||||
description: 'Mô phỏng trường hợp AIHub gặp sự cố, hệ thống tự động bảo toàn điểm quy tắc và kích hoạt cờ degraded.',
|
|
||||||
expectedStatus: 'completed',
|
|
||||||
expectedMode: 'rules_only_fallback',
|
|
||||||
payload: {
|
|
||||||
tenant_id: 'omni-test-tenant-42',
|
|
||||||
subject_ref: 'ems-lead-dev-1003',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
snapshot: {
|
|
||||||
version: '1.0.0',
|
|
||||||
scope: 'linked_contact_history',
|
|
||||||
captured_at: '2026-08-28T12:10:00Z',
|
|
||||||
lead_profile: {
|
|
||||||
ems_lead_id: 'ems-lead-dev-1003',
|
|
||||||
name: 'Le Van Cuong',
|
|
||||||
phone: '+84987654321',
|
|
||||||
email: 'cuong.le@example.com',
|
|
||||||
status: 'in_progress',
|
|
||||||
},
|
|
||||||
conversations: [
|
|
||||||
{
|
|
||||||
conversation_id: 'conv-web-301',
|
|
||||||
channel: 'website',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
message_id: 'msg-05',
|
|
||||||
sender_type: 'contact',
|
|
||||||
sent_at: '2026-08-28T12:09:00Z',
|
|
||||||
content: 'Toi can tu van goi doanh nghiep.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hitl_policy: {
|
|
||||||
policy_version: '1.0.0',
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'aihub-timeout',
|
|
||||||
name: '4. AIHub timeout',
|
|
||||||
category: 'Mô phỏng lỗi hạ tầng',
|
|
||||||
coverImage: '/images/lead-system-fault.jpg',
|
|
||||||
description: 'Fixture kiểm thử policy khi request AIHub vượt quá timeout đã cấu hình.',
|
|
||||||
expectedStatus: 'failed',
|
|
||||||
payload: {
|
|
||||||
tenant_id: 'omni-test-tenant-42',
|
|
||||||
subject_ref: 'ems-lead-dev-1004',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
snapshot: {
|
|
||||||
version: '1.0.0',
|
|
||||||
scope: 'linked_contact_history',
|
|
||||||
captured_at: '2026-08-28T12:15:00Z',
|
|
||||||
lead_profile: {
|
|
||||||
ems_lead_id: 'ems-lead-dev-1004',
|
|
||||||
name: 'Pham D',
|
|
||||||
phone: null,
|
|
||||||
email: null,
|
|
||||||
status: 'spam',
|
|
||||||
},
|
|
||||||
conversations: [],
|
|
||||||
hitl_policy: {
|
|
||||||
policy_version: '1.0.0',
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'aihub-quota-failure',
|
|
||||||
name: '5. AIHub quota failure',
|
|
||||||
category: 'Mô phỏng lỗi hạ tầng',
|
|
||||||
coverImage: '/images/lead-system-fault.jpg',
|
|
||||||
description: 'Fixture kiểm thử policy khi AIHub trả lỗi hết quota hoặc rate limit.',
|
|
||||||
expectedStatus: 'failed',
|
|
||||||
payload: {
|
|
||||||
tenant_id: 'omni-test-tenant-42',
|
|
||||||
subject_ref: 'ems-lead-dev-1005',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
snapshot: {
|
|
||||||
version: '1.0.0',
|
|
||||||
scope: 'linked_contact_history',
|
|
||||||
captured_at: '2026-08-28T12:20:00Z',
|
|
||||||
lead_profile: {
|
|
||||||
ems_lead_id: 'ems-lead-dev-1005',
|
|
||||||
name: 'Quota Failure Fixture',
|
|
||||||
phone: null,
|
|
||||||
email: null,
|
|
||||||
status: 'open',
|
|
||||||
},
|
|
||||||
conversations: [],
|
|
||||||
hitl_policy: {
|
|
||||||
policy_version: '1.0.0',
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'aihub-malformed-output',
|
|
||||||
name: '6. AIHub malformed output',
|
|
||||||
category: 'Mô phỏng lỗi contract',
|
|
||||||
coverImage: '/images/lead-system-fault.jpg',
|
|
||||||
description: 'Fixture kiểm thử khi AIHub trả payload không khớp bounded output contract.',
|
|
||||||
expectedStatus: 'failed',
|
|
||||||
payload: {
|
|
||||||
tenant_id: 'omni-test-tenant-42',
|
|
||||||
subject_ref: 'ems-lead-dev-1006',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
snapshot: {
|
|
||||||
version: '1.0.0',
|
|
||||||
scope: 'linked_contact_history',
|
|
||||||
captured_at: '2026-08-28T12:25:00Z',
|
|
||||||
lead_profile: {
|
|
||||||
ems_lead_id: 'ems-lead-dev-1006',
|
|
||||||
name: 'Malformed Output Fixture',
|
|
||||||
phone: null,
|
|
||||||
email: null,
|
|
||||||
status: 'open',
|
|
||||||
},
|
|
||||||
conversations: [],
|
|
||||||
hitl_policy: {
|
|
||||||
policy_version: '1.0.0',
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,201 +0,0 @@
|
|||||||
import {
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
admitScoringRun,
|
|
||||||
createApiClient,
|
|
||||||
fetchExecutionTrace,
|
|
||||||
fetchScoringRun,
|
|
||||||
listScoringConfigs,
|
|
||||||
listScoringRuns,
|
|
||||||
pollUntilTerminal,
|
|
||||||
saveScoringConfig,
|
|
||||||
} from '../api/client';
|
|
||||||
import type {
|
|
||||||
CreateScoringRunRequest,
|
|
||||||
DevConsoleConfig,
|
|
||||||
GoldenFixture,
|
|
||||||
ScoringConfig,
|
|
||||||
ScoringRunResponse,
|
|
||||||
} from '../api/types';
|
|
||||||
import { GOLDEN_FIXTURES } from '../fixtures/golden';
|
|
||||||
|
|
||||||
export class DevConsoleState {
|
|
||||||
config = $state<DevConsoleConfig>({ ...DEFAULT_CONFIG });
|
|
||||||
selectedFixture = $state<GoldenFixture>(GOLDEN_FIXTURES[0]);
|
|
||||||
customPayloadJson = $state<string>(JSON.stringify(GOLDEN_FIXTURES[0].payload, null, 2));
|
|
||||||
scoringConfigs = $state<ScoringConfig[]>([]);
|
|
||||||
selectedScoringConfig = $state<ScoringConfig | null>(null);
|
|
||||||
executionTrace = $state<Record<string, unknown> | null>(null);
|
|
||||||
|
|
||||||
// Current active run state
|
|
||||||
currentRunId = $state<string | null>(null);
|
|
||||||
currentRun = $state<ScoringRunResponse | null>(null);
|
|
||||||
isLoading = $state<boolean>(false);
|
|
||||||
isPolling = $state<boolean>(false);
|
|
||||||
errorMessage = $state<string | null>(null);
|
|
||||||
statusMessage = $state<string>('Sẵn sàng');
|
|
||||||
|
|
||||||
recentRuns = $state<ScoringRunResponse[]>([]);
|
|
||||||
totalRunCount = $state<number>(0);
|
|
||||||
isLoadingRuns = $state<boolean>(false);
|
|
||||||
readonly runPageSize = 30;
|
|
||||||
currentRunPage = $state<number>(1);
|
|
||||||
hasNextRunPage = $state<boolean>(false);
|
|
||||||
|
|
||||||
async loadRecentRuns(page: number = this.currentRunPage) {
|
|
||||||
this.isLoadingRuns = true;
|
|
||||||
try {
|
|
||||||
const offset = (page - 1) * this.runPageSize;
|
|
||||||
const result = await listScoringRuns(
|
|
||||||
createApiClient(),
|
|
||||||
this.config.tenantId,
|
|
||||||
this.runPageSize + 1,
|
|
||||||
offset,
|
|
||||||
);
|
|
||||||
this.currentRunPage = page;
|
|
||||||
this.totalRunCount = result.total;
|
|
||||||
this.hasNextRunPage = result.items.length > this.runPageSize;
|
|
||||||
this.recentRuns = result.items.slice(0, this.runPageSize);
|
|
||||||
} finally {
|
|
||||||
this.isLoadingRuns = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async nextRunPage() {
|
|
||||||
if (!this.hasNextRunPage || this.isLoadingRuns) return;
|
|
||||||
await this.loadRecentRuns(this.currentRunPage + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async previousRunPage() {
|
|
||||||
if (this.currentRunPage <= 1 || this.isLoadingRuns) return;
|
|
||||||
await this.loadRecentRuns(this.currentRunPage - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectRunById(runId: string, tenantId: string = this.config.tenantId) {
|
|
||||||
this.currentRunId = runId;
|
|
||||||
this.isLoading = true;
|
|
||||||
this.errorMessage = null;
|
|
||||||
try {
|
|
||||||
const client = createApiClient();
|
|
||||||
const res = await fetchScoringRun(client, runId, tenantId);
|
|
||||||
if (res.data) {
|
|
||||||
this.currentRun = res.data;
|
|
||||||
this.statusMessage = `Đã chọn phiên ${runId} (${res.data.status})`;
|
|
||||||
try {
|
|
||||||
this.executionTrace = await fetchExecutionTrace(client, runId, tenantId);
|
|
||||||
} catch {
|
|
||||||
this.executionTrace = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errorMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadScoringConfigs() {
|
|
||||||
const configs = await listScoringConfigs(createApiClient(), this.config);
|
|
||||||
this.scoringConfigs = configs;
|
|
||||||
this.selectedScoringConfig ??= configs[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveScoringConfig(scoringConfig: ScoringConfig) {
|
|
||||||
const saved = await saveScoringConfig(createApiClient(), scoringConfig, this.config);
|
|
||||||
this.scoringConfigs = [...this.scoringConfigs, saved];
|
|
||||||
this.selectedScoringConfig = saved;
|
|
||||||
return saved;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectFixture(fixture: GoldenFixture) {
|
|
||||||
this.selectedFixture = fixture;
|
|
||||||
this.customPayloadJson = JSON.stringify(fixture.payload, null, 2);
|
|
||||||
this.errorMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerRun() {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.errorMessage = null;
|
|
||||||
this.statusMessage = 'Đang gửi yêu cầu tiếp nhận đánh giá lead...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
let parsedPayload: CreateScoringRunRequest;
|
|
||||||
try {
|
|
||||||
parsedPayload = JSON.parse(this.customPayloadJson);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Dữ liệu JSON payload không hợp lệ: ${String(err)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = createApiClient();
|
|
||||||
const res = await admitScoringRun(client, parsedPayload, {}, this.config);
|
|
||||||
|
|
||||||
if (!res.data || res.status !== 202) {
|
|
||||||
throw new Error(
|
|
||||||
`Yêu cầu bị từ chối (HTTP ${res.status}): ${JSON.stringify(res.error ?? 'Lỗi không xác định')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runId = res.data.run_id;
|
|
||||||
this.currentRunId = runId;
|
|
||||||
this.statusMessage = `Đã tiếp nhận phiên ${runId} (trạng thái: ${res.data.status}). Đang theo dõi tiến trình...`;
|
|
||||||
|
|
||||||
// Start polling
|
|
||||||
this.isPolling = true;
|
|
||||||
const finalRun = await pollUntilTerminal(client, runId, {
|
|
||||||
tenantId: parsedPayload.tenant_id,
|
|
||||||
intervalMs: this.config.pollIntervalMs,
|
|
||||||
onTick: (run) => {
|
|
||||||
this.currentRun = run;
|
|
||||||
this.statusMessage = `Trạng thái phiên: ${run.status}...`;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentRun = finalRun;
|
|
||||||
this.statusMessage = `Phiên đánh giá kết thúc với trạng thái: ${finalRun.status}`;
|
|
||||||
this.totalRunCount += 1;
|
|
||||||
if (this.currentRunPage === 1) {
|
|
||||||
const pageWasFull = this.recentRuns.length === this.runPageSize;
|
|
||||||
this.recentRuns = [finalRun, ...this.recentRuns.filter((r) => r.run_id !== finalRun.run_id)].slice(
|
|
||||||
0,
|
|
||||||
this.runPageSize,
|
|
||||||
);
|
|
||||||
this.hasNextRunPage = this.hasNextRunPage || pageWasFull;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.executionTrace = await fetchExecutionTrace(client, runId, parsedPayload.tenant_id);
|
|
||||||
} catch {
|
|
||||||
this.executionTrace = null;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errorMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
this.statusMessage = 'Thực thi thất bại';
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.isPolling = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshRun() {
|
|
||||||
if (!this.currentRunId) return;
|
|
||||||
this.isLoading = true;
|
|
||||||
this.errorMessage = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const client = createApiClient();
|
|
||||||
const tenantId = this.currentRun?.tenant_id ?? this.config.tenantId;
|
|
||||||
const res = await fetchScoringRun(client, this.currentRunId, tenantId);
|
|
||||||
if (res.data) {
|
|
||||||
this.currentRun = res.data;
|
|
||||||
this.statusMessage = `Đã làm mới phiên ${this.currentRunId} (phiên bản: v${res.data.version})`;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Lỗi tải dữ liệu: ${JSON.stringify(res.error)}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errorMessage = err instanceof Error ? err.message : String(err);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const consoleState = new DevConsoleState();
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import './app.css';
|
|
||||||
import { mount } from 'svelte';
|
|
||||||
import App from './App.svelte';
|
|
||||||
|
|
||||||
const target = document.getElementById('app');
|
|
||||||
if (!target) {
|
|
||||||
throw new Error('Root #app container not found in DOM');
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = mount(App, {
|
|
||||||
target,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
@ -1,163 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
import {
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
admitScoringRun,
|
|
||||||
fetchScoringRun,
|
|
||||||
listScoringRuns,
|
|
||||||
pollUntilTerminal,
|
|
||||||
} from '../lib/api/client';
|
|
||||||
import type { CreateScoringRunRequest, ScoringRunResponse } from '../lib/api/types';
|
|
||||||
import { GOLDEN_FIXTURES } from '../lib/fixtures/golden';
|
|
||||||
|
|
||||||
describe('Dev Console API Client', () => {
|
|
||||||
it('sends only non-secret headers during run admission', async () => {
|
|
||||||
const mockPost = vi.fn().mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
run_id: 'test-run-123',
|
|
||||||
tenant_id: 'omni-test-tenant-42',
|
|
||||||
subject_ref: 'ems-lead-1',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
status: 'pending',
|
|
||||||
status_url: '/v1/scoring-runs/test-run-123',
|
|
||||||
created_at: '2026-08-28T12:00:00Z',
|
|
||||||
},
|
|
||||||
error: undefined,
|
|
||||||
response: { status: 202 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockClient = {
|
|
||||||
POST: mockPost,
|
|
||||||
GET: vi.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const payload: CreateScoringRunRequest = {
|
|
||||||
...GOLDEN_FIXTURES[0].payload,
|
|
||||||
rule_overrides: [
|
|
||||||
{
|
|
||||||
id: 'demo-rule-1',
|
|
||||||
name: 'Demo budget signal',
|
|
||||||
target: 'custom_field',
|
|
||||||
custom_field_key: 'budget',
|
|
||||||
operator: 'gte',
|
|
||||||
value: 5000000,
|
|
||||||
points: 15,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const res = await admitScoringRun(
|
|
||||||
mockClient,
|
|
||||||
payload,
|
|
||||||
{
|
|
||||||
idempotencyKey: 'idem-123',
|
|
||||||
dotbApp: 'dev-console',
|
|
||||||
dotbFeature: 'fixture-test',
|
|
||||||
},
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(res.status).toBe(202);
|
|
||||||
expect(res.data?.run_id).toBe('test-run-123');
|
|
||||||
expect(mockPost).toHaveBeenCalledWith('/v1/scoring-runs', {
|
|
||||||
params: {
|
|
||||||
header: {
|
|
||||||
'X-Tenant-VKey': '',
|
|
||||||
'Idempotency-Key': 'idem-123',
|
|
||||||
'X-DotB-App': 'dev-console',
|
|
||||||
'X-DotB-Feature': 'fixture-test',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: payload,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes pagination parameters and parses total count when listing scoring runs', async () => {
|
|
||||||
const mockHeaders = new Headers({
|
|
||||||
'x-total-count': '35',
|
|
||||||
});
|
|
||||||
const mockRuns = [{ run_id: 'run-1' }, { run_id: 'run-2' }] as ScoringRunResponse[];
|
|
||||||
const mockGet = vi.fn().mockResolvedValue({
|
|
||||||
data: mockRuns,
|
|
||||||
error: undefined,
|
|
||||||
response: { status: 200, headers: mockHeaders },
|
|
||||||
});
|
|
||||||
const mockClient = { GET: mockGet } as any;
|
|
||||||
|
|
||||||
const result = await listScoringRuns(mockClient, 'tenant-1', 31, 60);
|
|
||||||
|
|
||||||
expect(mockGet).toHaveBeenCalledWith('/v1/scoring-runs', {
|
|
||||||
params: {
|
|
||||||
header: { 'X-Tenant-Id': 'tenant-1' },
|
|
||||||
query: { limit: 31, offset: 60 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(result.items).toEqual(mockRuns);
|
|
||||||
expect(result.total).toBe(35);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('falls back to items length if X-Total-Count header is missing', async () => {
|
|
||||||
const mockRuns = [{ run_id: 'run-1' }] as ScoringRunResponse[];
|
|
||||||
const mockGet = vi.fn().mockResolvedValue({
|
|
||||||
data: mockRuns,
|
|
||||||
error: undefined,
|
|
||||||
response: { status: 200, headers: new Headers() },
|
|
||||||
});
|
|
||||||
const mockClient = { GET: mockGet } as any;
|
|
||||||
|
|
||||||
const result = await listScoringRuns(mockClient, 'tenant-1');
|
|
||||||
expect(result.items).toEqual(mockRuns);
|
|
||||||
expect(result.total).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('polls until reaching terminal status', async () => {
|
|
||||||
let callCount = 0;
|
|
||||||
const mockGet = vi.fn().mockImplementation(() => {
|
|
||||||
callCount++;
|
|
||||||
const status = callCount === 1 ? 'pending' : callCount === 2 ? 'running' : 'completed';
|
|
||||||
return Promise.resolve({
|
|
||||||
data: {
|
|
||||||
run_id: 'poll-run-1',
|
|
||||||
tenant_id: 'tenant-1',
|
|
||||||
subject_ref: 'lead-1',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
parent_run_id: null,
|
|
||||||
status,
|
|
||||||
rule_score: 80,
|
|
||||||
ai_score: 90,
|
|
||||||
final_score: 88,
|
|
||||||
effective_score: 88,
|
|
||||||
scoring_mode: 'medium_ai',
|
|
||||||
degraded: false,
|
|
||||||
ai_metadata: null,
|
|
||||||
confidence: 0.9,
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
effective_config: null,
|
|
||||||
error_code: null,
|
|
||||||
retryable: null,
|
|
||||||
version: 1,
|
|
||||||
started_at: '2026-08-28T12:00:00Z',
|
|
||||||
completed_at: '2026-08-28T12:00:05Z',
|
|
||||||
created_at: '2026-08-28T12:00:00Z',
|
|
||||||
updated_at: '2026-08-28T12:00:05Z',
|
|
||||||
} as ScoringRunResponse,
|
|
||||||
error: undefined,
|
|
||||||
response: { status: 200 },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockClient = {
|
|
||||||
GET: mockGet,
|
|
||||||
POST: vi.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const ticks: string[] = [];
|
|
||||||
const finalRun = await pollUntilTerminal(mockClient, 'poll-run-1', {
|
|
||||||
intervalMs: 5,
|
|
||||||
onTick: (run) => ticks.push(run.status),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(finalRun.status).toBe('completed');
|
|
||||||
expect(callCount).toBe(3);
|
|
||||||
expect(ticks).toEqual(['pending', 'running', 'completed']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { GOLDEN_FIXTURES } from '../lib/fixtures/golden';
|
|
||||||
|
|
||||||
describe('Golden Fixtures Catalog', () => {
|
|
||||||
it('contains all required pilot test scenarios', () => {
|
|
||||||
const fixtureIds = GOLDEN_FIXTURES.map((f) => f.id);
|
|
||||||
expect(fixtureIds).toContain('happy-path-medium-ai');
|
|
||||||
expect(fixtureIds).toContain('review-required-low-confidence');
|
|
||||||
expect(fixtureIds).toContain('degraded-rules-fallback');
|
|
||||||
expect(fixtureIds).toContain('aihub-timeout');
|
|
||||||
expect(fixtureIds).toContain('aihub-quota-failure');
|
|
||||||
expect(fixtureIds).toContain('aihub-malformed-output');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ensures each fixture has a valid CanonicalSnapshot structure', () => {
|
|
||||||
for (const fixture of GOLDEN_FIXTURES) {
|
|
||||||
expect(fixture.name).toBeTruthy();
|
|
||||||
expect(fixture.expectedStatus).toBeTruthy();
|
|
||||||
expect(fixture.payload.tenant_id).toBeTruthy();
|
|
||||||
expect(fixture.payload.scoring_config_version).toBe('1.0.0');
|
|
||||||
expect(fixture.payload.snapshot.version).toBe('1.0.0');
|
|
||||||
expect(fixture.payload.snapshot.captured_at).toBeTruthy();
|
|
||||||
expect(Array.isArray(fixture.payload.snapshot.conversations)).toBe(true);
|
|
||||||
expect(fixture.payload.snapshot.hitl_policy?.confidence_threshold).toBe(0.8);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
import * as clientModule from '../lib/api/client';
|
|
||||||
import { GOLDEN_FIXTURES } from '../lib/fixtures/golden';
|
|
||||||
import { DevConsoleState } from '../lib/state/consoleState.svelte';
|
|
||||||
|
|
||||||
describe('DevConsoleState Store', () => {
|
|
||||||
it('initializes with default fixture and configuration', () => {
|
|
||||||
const state = new DevConsoleState();
|
|
||||||
expect(state.config.tenantId).toBe('omni-account-42');
|
|
||||||
expect(state.selectedFixture.id).toBe(GOLDEN_FIXTURES[0].id);
|
|
||||||
expect(state.currentRun).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates custom payload JSON when fixture is selected', () => {
|
|
||||||
const state = new DevConsoleState();
|
|
||||||
const secondFixture = GOLDEN_FIXTURES[1];
|
|
||||||
state.selectFixture(secondFixture);
|
|
||||||
|
|
||||||
expect(state.selectedFixture.id).toBe(secondFixture.id);
|
|
||||||
const parsed = JSON.parse(state.customPayloadJson);
|
|
||||||
expect(parsed.subject_ref).toBe(secondFixture.payload.subject_ref);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('triggers run and records the terminal state', async () => {
|
|
||||||
const state = new DevConsoleState();
|
|
||||||
|
|
||||||
vi.spyOn(clientModule, 'admitScoringRun').mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
run_id: 'run-999',
|
|
||||||
tenant_id: 'tenant-1',
|
|
||||||
subject_ref: 'lead-1',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
status: 'pending',
|
|
||||||
status_url: '/v1/scoring-runs/run-999',
|
|
||||||
created_at: '2026-08-28T12:00:00Z',
|
|
||||||
},
|
|
||||||
status: 202,
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.spyOn(clientModule, 'pollUntilTerminal').mockResolvedValue({
|
|
||||||
run_id: 'run-999',
|
|
||||||
tenant_id: 'tenant-1',
|
|
||||||
subject_ref: 'lead-1',
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
parent_run_id: null,
|
|
||||||
status: 'completed',
|
|
||||||
rule_score: 80,
|
|
||||||
ai_score: 90,
|
|
||||||
final_score: 86,
|
|
||||||
effective_score: 86,
|
|
||||||
scoring_mode: 'medium_ai',
|
|
||||||
degraded: false,
|
|
||||||
ai_metadata: null,
|
|
||||||
confidence: 0.9,
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
effective_config: null,
|
|
||||||
error_code: null,
|
|
||||||
retryable: null,
|
|
||||||
version: 1,
|
|
||||||
started_at: '2026-08-28T12:00:00Z',
|
|
||||||
completed_at: '2026-08-28T12:00:05Z',
|
|
||||||
created_at: '2026-08-28T12:00:00Z',
|
|
||||||
updated_at: '2026-08-28T12:00:05Z',
|
|
||||||
});
|
|
||||||
|
|
||||||
await state.triggerRun();
|
|
||||||
|
|
||||||
expect(state.currentRunId).toBe('run-999');
|
|
||||||
expect(state.currentRun?.status).toBe('completed');
|
|
||||||
expect(state.totalRunCount).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads paginated run history, updates totalRunCount, and navigates between pages', async () => {
|
|
||||||
const runs = Array.from({ length: 31 }, (_, index) => ({
|
|
||||||
run_id: `run-${index}`,
|
|
||||||
tenant_id: 'tenant-1',
|
|
||||||
subject_ref: `lead-${index}`,
|
|
||||||
scoring_config_version: '1.0.0',
|
|
||||||
parent_run_id: null,
|
|
||||||
status: 'completed',
|
|
||||||
rule_score: 80,
|
|
||||||
ai_score: 90,
|
|
||||||
final_score: 86,
|
|
||||||
effective_score: 86,
|
|
||||||
scoring_mode: 'medium_ai',
|
|
||||||
degraded: false,
|
|
||||||
ai_metadata: null,
|
|
||||||
confidence: 0.9,
|
|
||||||
confidence_threshold: 0.8,
|
|
||||||
effective_config: null,
|
|
||||||
error_code: null,
|
|
||||||
retryable: null,
|
|
||||||
version: 1,
|
|
||||||
started_at: '2026-08-28T12:00:00Z',
|
|
||||||
completed_at: '2026-08-28T12:00:05Z',
|
|
||||||
created_at: '2026-08-28T12:00:00Z',
|
|
||||||
updated_at: '2026-08-28T12:00:05Z',
|
|
||||||
})) as any;
|
|
||||||
const listSpy = vi.spyOn(clientModule, 'listScoringRuns').mockImplementation(async (_client, _tenant, limit, offset) => {
|
|
||||||
expect(limit).toBe(31);
|
|
||||||
return {
|
|
||||||
items: offset === 0 ? runs : runs.slice(30, 31),
|
|
||||||
total: 35,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const state = new DevConsoleState();
|
|
||||||
|
|
||||||
await state.loadRecentRuns();
|
|
||||||
expect(state.recentRuns).toHaveLength(30);
|
|
||||||
expect(state.totalRunCount).toBe(35);
|
|
||||||
expect(state.hasNextRunPage).toBe(true);
|
|
||||||
await state.nextRunPage();
|
|
||||||
expect(state.currentRunPage).toBe(2);
|
|
||||||
expect(state.recentRuns).toHaveLength(1);
|
|
||||||
expect(state.totalRunCount).toBe(35);
|
|
||||||
expect(state.hasNextRunPage).toBe(false);
|
|
||||||
await state.previousRunPage();
|
|
||||||
expect(state.currentRunPage).toBe(1);
|
|
||||||
expect(state.totalRunCount).toBe(35);
|
|
||||||
expect(listSpy).toHaveBeenCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
preprocess: vitePreprocess(),
|
|
||||||
};
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"paths": {
|
|
||||||
"$lib": ["./src/lib"],
|
|
||||||
"$lib/*": ["./src/lib/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import path from 'node:path';
|
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
||||||
import { defineConfig, loadEnv, type ProxyOptions } from 'vite';
|
|
||||||
|
|
||||||
export default defineConfig(({ command, mode }) => {
|
|
||||||
const env = loadEnv(mode, process.cwd(), 'DEV_CONSOLE_');
|
|
||||||
const target = env.DEV_CONSOLE_API_URL || 'http://localhost:8000';
|
|
||||||
const serviceToken = env.DEV_CONSOLE_SERVICE_TOKEN;
|
|
||||||
const testVKey = env.DEV_CONSOLE_TEST_VKEY;
|
|
||||||
const port = Number(env.DEV_CONSOLE_PORT) || 5173;
|
|
||||||
const previewPort = Number(env.DEV_CONSOLE_PREVIEW_PORT) || 4173;
|
|
||||||
|
|
||||||
const missingCredentials = command === 'serve' && (!serviceToken || !testVKey);
|
|
||||||
const proxy: Record<string, ProxyOptions> = {
|
|
||||||
'/dev-console-api': {
|
|
||||||
target,
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (requestPath) => requestPath.replace(/^\/dev-console-api/, ''),
|
|
||||||
configure: (proxyServer) => {
|
|
||||||
proxyServer.on('proxyReq', (proxyRequest, request) => {
|
|
||||||
if (missingCredentials || !serviceToken || !testVKey) {
|
|
||||||
proxyRequest.destroy(
|
|
||||||
new Error('Dev Console proxy credentials are not configured on the server.'),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
proxyRequest.setHeader('Authorization', `Bearer ${serviceToken}`);
|
|
||||||
const requestPath = request.url?.split('?', 1)[0] ?? '';
|
|
||||||
if (
|
|
||||||
request.method === 'POST' &&
|
|
||||||
/\/v1\/scoring-runs(?:\/|$)/.test(requestPath)
|
|
||||||
) {
|
|
||||||
proxyRequest.setHeader('X-Tenant-VKey', testVKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
plugins: [svelte()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
$lib: path.resolve(__dirname, './src/lib'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port,
|
|
||||||
proxy,
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
port: previewPort,
|
|
||||||
proxy,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: lead_scoring
|
|
||||||
ports:
|
|
||||||
- "${POSTGRES_PORT:-5433}:5432"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres -d lead_scoring"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
793c0f914c810bacd6c56cc0c94894d6f80230833080bbe6b376d5ce23bf8ed5 compatibility.json
|
|
||||||
63a84bbc9afd6850787c16d8e9231f7810a805b677e9ee928a4ed353183a03fe contract.json
|
|
||||||
6c7fe40f9c43209d1e4d54e0bc294bf29b1ceb5bf1fee26ee36cf383564a6e8e error-taxonomy.json
|
|
||||||
5e4dee334c736f404ab3e8cf9cbadf4dfaa06111a3cd9b194463efdd0d5ff8cd fixture-index.json
|
|
||||||
e83645e1d875320a2845d823d71f43542689561de2abf3d5f8cc50df62a53300 fixtures/admission-request.json
|
|
||||||
09e599fc89b99b6538a1f505cbdd0c71c89bc0a4cf71b8f80f9069a1d28f7134 fixtures/admission-response.json
|
|
||||||
84de8353436a8135c1ad963e00fe666b457dc9bba1861e5043e1a61cff7ea910 fixtures/error-validation.json
|
|
||||||
48963cc3ee55960fc60c6ec71da773af4afc49af9b9b46f8cb3fa947e2ef710f fixtures/lineage-child.json
|
|
||||||
54bdb599cd84f30965f85fe4089ac4839b120d86d1755c3d9052cfd3b0884cf2 fixtures/lineage-root.json
|
|
||||||
f41fec32608f130e5780e9c6594df3d12e617eca82677d20c6d410055866675c fixtures/marker/deleted-block-followed-by-append.json
|
|
||||||
8340b28688c6037f673f20a072555c67468aa2e070922982c11ae49c91c26c9e fixtures/marker/empty-description-append.json
|
|
||||||
1914b2fdb1e0cff6e27724c94351bfbc9975020307b2009ab56242970bbaf026 fixtures/marker/failed-run-no-render.json
|
|
||||||
10158b934279d4e415913d06cc3418b802007f0db970f6043edb53a6b0558b3a fixtures/marker/multiple-complete-blocks.json
|
|
||||||
a5774525903f73c49d88f23ca86d8eb298de6e2fbf67db30ad523c7b066212f8 fixtures/marker/nested-block.json
|
|
||||||
452eb1f3806bb8d98a3e7c3a246ecca5e0b4f8cf7566e7afc7742e6431af0b04 fixtures/marker/orphan-closing.json
|
|
||||||
a7ad66c859fe2076a9418b894273130aa7e0221f34de17387f2d640a18f7e951 fixtures/marker/orphan-opening.json
|
|
||||||
5cab508bc921a4fac2208b9575798108730e39ae5f4adbca94111ffd8a08c18c fixtures/marker/overlapping-block.json
|
|
||||||
e5b9d9ca88103a08a888aa22941c2c27f1fad31300a0a4dbd00ab4003c1446a7 fixtures/marker/plain-description-append.json
|
|
||||||
63a1665bdd49eb1e8fe3513480d98a73bd990c2df56a59c2ca21a6d3719d16ab fixtures/marker/single-valid-block-replace.json
|
|
||||||
dddf775f42e1689a40d3d4b94adc00ed22153f0bb4819c5ca7d63b988ecbfe6f fixtures/marker/unicode-multiline-preservation.json
|
|
||||||
a2deb1c0ccd2cd81f940a30d70945a9d37be721606324025ed2c01ac5062afb5 fixtures/negative/admission-subject-mismatch.json
|
|
||||||
073565a826bc5d6d43159bcd0a4d553d7efe2ccc2203a5b09a053e143f09e9cb fixtures/negative/event-missing-version.json
|
|
||||||
c3b97e49b3a4e26aaef82f5c6eddb4927b92cd85f253ed89cff20aa209e6024d fixtures/negative/review-override-out-of-range.json
|
|
||||||
c6093699d1997dc9d349ba3afded1b9ba9fa68d0fe1bb7c32f9e7dcd49940c2b fixtures/negative/snapshot-future-message.json
|
|
||||||
480aa257f0813debafdbf37a800385a2cc8e74c2e553ac01ab5f8abd70fd227e fixtures/polling/completed.json
|
|
||||||
98c4c933f593920a822cb22c8b50023a247deb34402adbce14a27f4c6c3be1cc fixtures/polling/failed.json
|
|
||||||
6dd6788d7e95d0ec797dd6f890c31a9c3e293e5c71b302a4076a7fbc85e26a3d fixtures/polling/pending.json
|
|
||||||
5f8238d989c886eead741ac07341f763f2cb7cac6fe3000c960fbc776020f78f fixtures/polling/running.json
|
|
||||||
419b8b71c5424419fbb39bdbdaf5f8d3bef3405e841dfcff19f9670405867f89 fixtures/review-approve-request.json
|
|
||||||
1389e2affd2b927b4437491b24c2fb0bc0e48214b0a9c722e2eee830e802fca8 fixtures/review-override-request.json
|
|
||||||
2a6934fc8f56694f70e440cfb7ca450c428353fde6ecc0bf919d15487ca86f30 fixtures/review-re_evaluate-request.json
|
|
||||||
2019d35cbd2a1a583a78be5c9e00ddcf21db65ad9bab06835c24c4870b4de952 fixtures/review-response.json
|
|
||||||
604335dd696d3f9c802c55bfa2a195857a839d8169f5060a2541f166b2245fcb fixtures/scoring-config-selection.json
|
|
||||||
b45fc82c1bbf1eea453228ba62f58a2de1509d7493ba902a162a8ca10f6f3d25 fixtures/scoring-config.json
|
|
||||||
d25239c09bd12d1156febc43700aa57791910240b89f822154cfcb9876c460a6 fixtures/snapshot.json
|
|
||||||
c3e3c743733cd937e4ae49a45a111d25d11982b1ad68c7af63b1ee857feac6b7 fixtures/tenant-unavailable.json
|
|
||||||
bb51676f91a9bde91d391e72154753e6abe55827a25f7d22db15a535f4ec3196 fixtures/webhook/completed.json
|
|
||||||
990cea2d63e7fcfa3fbe6dd6ba2412c6dd8de5f038b441e985a1e0191d0d4752 fixtures/webhook/review_required.json
|
|
||||||
b24c4046a0cd00f2f0211e44bee4fdeab10309d8126bcf117d5569d07f27cef4 fixtures/webhook/reviewed_approve.json
|
|
||||||
1a1bab449d6edb7a857cb032fed676e19a08db62431cedecec5bd25b2180a663 fixtures/webhook/reviewed_override.json
|
|
||||||
260f6668bf924b75c63d114ca3de0851af2fc7f85e42a1fa1d0d3fd35892a267 fixtures/webhook/reviewed_reevaluate.json
|
|
||||||
c3e3c743733cd937e4ae49a45a111d25d11982b1ad68c7af63b1ee857feac6b7 fixtures/webhook/tenant-unavailable.json
|
|
||||||
e81e98039aa66cdb612e7f804c3e4f4c6f3de2d96a903fe6cde8924e02c1a694 fixtures/webhook/worker_interrupted.json
|
|
||||||
8600e01feecf7f013813b02b2b5d8e39de76cb87b8c6ee135d929733aa683a4a openapi.json
|
|
||||||
88736cd21430f2c351d8500602ed382e43378d515deba78899fc0e0149ca3233 outcome-taxonomy.json
|
|
||||||
8558287dedb710f4f34f17a91c1bc71246a3ec29a4bac41359b3ffe862e1799e profiles/polling/contract.json
|
|
||||||
25e29cb91646ec1aa1216175d47569e6a857d79a5465d843702c0ce084b28288 profiles/polling/error-response.schema.json
|
|
||||||
adb529959e243d8de718ea74d17836b593a52990317321bddf9c4b7c195cac1b profiles/polling/scoring-run-response.schema.json
|
|
||||||
8774cac80f857e5bc3d89510f0bda43a5f2e9ac1a23b1318ff9372e52e9bd7da profiles/webhook/contract.json
|
|
||||||
a15d7e56ffca5b1f1e85a646da2bfee24b601b99bc0d065fa0ce578dd7d242a3 profiles/webhook/lead-scoring-reviewed-event.schema.json
|
|
||||||
5f64d8ef39c562a989dc54249c8617807c58c827e39a66f801f6eae08afd808b profiles/webhook/lead-scoring-terminal-event.schema.json
|
|
||||||
2ee23597c2108a7127587bfb1fe9d8969b9ad12e47f190c8ab3dcdb5dc228b0c profiles/webhook/signature-vectors.json
|
|
||||||
1c481f7e9143f4b76032684aaf1110897d5c1b34d86a8ec5a093f52c4eaea845 profiles/webhook/webhook-ingest-error-response.schema.json
|
|
||||||
7e23ffb6849f8e5a09770076772cfdbc2bc6fed3be7c4970a21d94f2a9985429 profiles/webhook/webhook-ingest-response.schema.json
|
|
||||||
385f9a5f3b91f4f173461f3ec7d0f5ee582fcfec0a03bf8aa2b878f5ce41850f schemas/admission-request.schema.json
|
|
||||||
e61fed87719d6167dfaeb958d6fa823f2c1acd6171e874587b352aef006f554c schemas/admission-response.schema.json
|
|
||||||
65f15af5c0d504ba98038a6b66dc1fe5726fbf482d81ea33e539b33490871f1e schemas/canonical-snapshot.schema.json
|
|
||||||
25e29cb91646ec1aa1216175d47569e6a857d79a5465d843702c0ce084b28288 schemas/error-response.schema.json
|
|
||||||
eb181d8cc5686fb7acfce0e73eb6a8d79b7ac79f67c5423d964a4e47becc7798 schemas/error-taxonomy.schema.json
|
|
||||||
d2eb05b9daaf72bd3a5f40ee2d3ee2f5d874151362b1e7c9b761d5daaf190ff8 schemas/lineage.schema.json
|
|
||||||
817d2dad9164444ee11ca1b625637eac2d2420de54bda2645b6788b2393cc675 schemas/outcome-taxonomy.schema.json
|
|
||||||
9d494587710850e820afa88b311fd79afdfe6e5416a983fa2b4fe9e8afa72563 schemas/polling-response.schema.json
|
|
||||||
86afdeab368f368c37969f97dfcd8abdb6b7d10652d1f209dd206e9f97dd66af schemas/review-request.schema.json
|
|
||||||
5b82fde8c08b675a9df649311b6dbe525683a15c95261a3dd3c71350fa0085d4 schemas/review-response.schema.json
|
|
||||||
a15d7e56ffca5b1f1e85a646da2bfee24b601b99bc0d065fa0ce578dd7d242a3 schemas/reviewed-event.schema.json
|
|
||||||
dccbdc230b64a24f5ed08c10c6ef9ff6fe85fcc1315bae3f0b0bc9541192cd25 schemas/scoring-config-selection.schema.json
|
|
||||||
2b45ded736a66ffc18ff6bf68c6d5ca77629613c9746ff6f38bb7687c4d7b23c schemas/scoring-config.schema.json
|
|
||||||
5f64d8ef39c562a989dc54249c8617807c58c827e39a66f801f6eae08afd808b schemas/terminal-event.schema.json
|
|
||||||
dcd5f6a13eed8717aad229e9a90b63c41488feee3e33f36e6bb0ec8483b82d6c schemas/webhook-event-envelope.schema.json
|
|
||||||
1c481f7e9143f4b76032684aaf1110897d5c1b34d86a8ec5a093f52c4eaea845 schemas/webhook-ingest-error-response.schema.json
|
|
||||||
7e23ffb6849f8e5a09770076772cfdbc2bc6fed3be7c4970a21d94f2a9985429 schemas/webhook-ingest-response.schema.json
|
|
||||||
44ed40efd0057f703855ef1b3e3b4103abf64b4123153877f886ab3599c42c6a security-profile.json
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"api_major_version": "v1",
|
|
||||||
"consumer": "dotb-omni",
|
|
||||||
"contract_pack_version": "0.1.0",
|
|
||||||
"pinning": {
|
|
||||||
"consumer_check": "Run Omni consumer tests against the pinned fixtures/ directory",
|
|
||||||
"fixture_corpus": "fixtures/",
|
|
||||||
"provider_check": "uv run generate-contract-pack --check",
|
|
||||||
"required_before_implementation": true,
|
|
||||||
"unit": "contract_pack_version_and_ai_platform_git_commit"
|
|
||||||
},
|
|
||||||
"provider": "ai-platform",
|
|
||||||
"upgrade": {
|
|
||||||
"breaking": {
|
|
||||||
"rule": "Publish a new API major directory and contract pack version; never mutate a pinned pack in place.",
|
|
||||||
"verification": "Run provider and consumer fixture validation before changing the Omni pin."
|
|
||||||
},
|
|
||||||
"compatible_additive": {
|
|
||||||
"rule": "Keep the same major version and preserve existing required fields and enum values.",
|
|
||||||
"verification": "Validate current fixtures with the previous consumer schema and add a fixture for the new optional field."
|
|
||||||
},
|
|
||||||
"sequence": [
|
|
||||||
"Generate and review artifacts from Pydantic source of truth.",
|
|
||||||
"Run provider --check and fixture validation.",
|
|
||||||
"Publish the immutable pack and record its AI Platform Git commit.",
|
|
||||||
"Update Omni's pin and run consumer validation against the same corpus.",
|
|
||||||
"Promote the pin only after sandbox compatibility verification."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
"api_major_version": "v1",
|
|
||||||
"artifacts": {
|
|
||||||
"checksums": "SHA256SUMS",
|
|
||||||
"compatibility": "compatibility.json",
|
|
||||||
"error_taxonomy": "error-taxonomy.json",
|
|
||||||
"fixture_index": "fixture-index.json",
|
|
||||||
"fixtures_directory": "fixtures/",
|
|
||||||
"manifest": "contract.json",
|
|
||||||
"openapi": "openapi.json",
|
|
||||||
"outcome_taxonomy": "outcome-taxonomy.json",
|
|
||||||
"schemas_directory": "schemas/",
|
|
||||||
"security_profile": "security-profile.json"
|
|
||||||
},
|
|
||||||
"contract_pack_version": "0.1.0",
|
|
||||||
"generator": "uv run generate-contract-pack",
|
|
||||||
"profiles": {
|
|
||||||
"admission": {
|
|
||||||
"request_schema_file": "schemas/admission-request.schema.json",
|
|
||||||
"response_schema_file": "schemas/admission-response.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"canonical_snapshot": {
|
|
||||||
"schema_file": "schemas/canonical-snapshot.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"outcome_schema_file": "schemas/outcome-taxonomy.schema.json",
|
|
||||||
"schema_file": "schemas/error-taxonomy.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"lineage": {
|
|
||||||
"schema_file": "schemas/lineage.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"polling": {
|
|
||||||
"endpoint": "GET /v1/scoring-runs/{run_id}",
|
|
||||||
"schema_file": "schemas/polling-response.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"review": {
|
|
||||||
"endpoint": "POST /v1/scoring-runs/{run_id}/reviews",
|
|
||||||
"request_schema_file": "schemas/review-request.schema.json",
|
|
||||||
"response_schema_file": "schemas/review-response.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"scoring_config": {
|
|
||||||
"immutable": true,
|
|
||||||
"schema_file": "schemas/scoring-config.schema.json",
|
|
||||||
"selection": "Callers select an existing version; Lead Scoring remains authoritative.",
|
|
||||||
"selection_schema_file": "schemas/scoring-config-selection.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"webhook": {
|
|
||||||
"endpoint": "POST /api/v1/internal/lead-scoring/events",
|
|
||||||
"schema_file": "schemas/webhook-event-envelope.schema.json",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"release_status": "pre-release",
|
|
||||||
"source_of_truth": "Pydantic models under src/lead_scoring/schemas"
|
|
||||||
}
|
|
||||||
@ -1,252 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"description": "Missing or invalid service authentication token.",
|
|
||||||
"error_code": "invalid_token",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 401,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Authenticated caller is not authorized for the requested tenant or operation.",
|
|
||||||
"error_code": "forbidden",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 403,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Requested scoring run is not available in the tenant scope.",
|
|
||||||
"error_code": "not_found",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 404,
|
|
||||||
"lifecycle": "polling",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "An idempotency key was reused with a different normalized request.",
|
|
||||||
"error_code": "idempotency_payload_mismatch",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 409,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Review expected version does not match the current run version.",
|
|
||||||
"error_code": "stale_run_version",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 409,
|
|
||||||
"lifecycle": "review",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "The current run state does not accept a review command.",
|
|
||||||
"error_code": "run_not_reviewable",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 409,
|
|
||||||
"lifecycle": "review",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Request headers, path parameters, or body failed contract validation.",
|
|
||||||
"error_code": "validation_error",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 422,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "A raw tenant credential was found in a persistent request field.",
|
|
||||||
"error_code": "credential_in_payload",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 422,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "The requested immutable scoring configuration version is unavailable.",
|
|
||||||
"error_code": "scoring_config_not_found",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 422,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Snapshot HITL policy does not match the selected scoring configuration.",
|
|
||||||
"error_code": "hitl_policy_mismatch",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 422,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Configured worker lease cannot cover the selected AI profile timeout.",
|
|
||||||
"error_code": "worker_lease_too_short",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 422,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Re-evaluate requires a tenant V-Key for the new execution attempt.",
|
|
||||||
"error_code": "missing_tenant_vkey",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 422,
|
|
||||||
"lifecycle": "review",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "The re-evaluation snapshot belongs to a different subject.",
|
|
||||||
"error_code": "subject_ref_mismatch",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 422,
|
|
||||||
"lifecycle": "review",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "The operational kill switch currently blocks new scoring initiation.",
|
|
||||||
"error_code": "scoring_admission_disabled",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 503,
|
|
||||||
"lifecycle": "admission",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": true,
|
|
||||||
"transport": "http"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "The execution worker lost the in-memory attempt before terminal completion.",
|
|
||||||
"error_code": "worker_interrupted",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "lead_scoring",
|
|
||||||
"retryable": true,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "AIHub rejected tenant credential authentication.",
|
|
||||||
"error_code": "aihub_auth_failed",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": 401,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "AIHub quota or rate limit was exceeded.",
|
|
||||||
"error_code": "aihub_quota_exceeded",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": 429,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "The bounded AIHub request timed out.",
|
|
||||||
"error_code": "aihub_timeout",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "AIHub returned a server-side failure.",
|
|
||||||
"error_code": "aihub_server_error",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "AIHub output did not conform to the required bounded result schema.",
|
|
||||||
"error_code": "aihub_invalid_output",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "The bounded AIHub request could not reach the service.",
|
|
||||||
"error_code": "aihub_network_error",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "AIHub returned a non-retryable client response.",
|
|
||||||
"error_code": "aihub_client_error",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "AIHub returned an unexpected normalized failure.",
|
|
||||||
"error_code": "aihub_unknown_error",
|
|
||||||
"fallback_allowed": true,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "execution",
|
|
||||||
"owner": "aihub",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "async_run"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Webhook destination is not available for the resolved tenant.",
|
|
||||||
"error_code": "subscriber_tenant_unavailable",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": 404,
|
|
||||||
"lifecycle": "webhook_ingest",
|
|
||||||
"owner": "dotb_omni",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "webhook"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "EMS description contains an orphan or multiple Marker Blocks.",
|
|
||||||
"error_code": "marker_block_malformed",
|
|
||||||
"fallback_allowed": false,
|
|
||||||
"http_status": null,
|
|
||||||
"lifecycle": "writeback",
|
|
||||||
"owner": "dotb_omni",
|
|
||||||
"retryable": false,
|
|
||||||
"transport": "writeback"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
{
|
|
||||||
"consumer_validation": "Consume the same paths without copying or rewriting payloads.",
|
|
||||||
"contract_pack_version": "0.1.0",
|
|
||||||
"marker": [
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/plain-description-append.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/empty-description-append.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/single-valid-block-replace.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/orphan-opening.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/orphan-closing.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/nested-block.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/overlapping-block.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/multiple-complete-blocks.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/unicode-multiline-preservation.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/deleted-block-followed-by-append.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/marker/failed-run-no-render.json",
|
|
||||||
"role": "provider_consumer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"negative": [
|
|
||||||
{
|
|
||||||
"path": "fixtures/negative/snapshot-future-message.json",
|
|
||||||
"schema": "schemas/canonical-snapshot.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/negative/admission-subject-mismatch.json",
|
|
||||||
"schema": "schemas/admission-request.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/negative/review-override-out-of-range.json",
|
|
||||||
"schema": "schemas/review-request.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/negative/event-missing-version.json",
|
|
||||||
"schema": "schemas/terminal-event.schema.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"positive": [
|
|
||||||
{
|
|
||||||
"path": "fixtures/snapshot.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/canonical-snapshot.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/scoring-config.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/scoring-config.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/scoring-config-selection.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/scoring-config-selection.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/admission-request.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/admission-request.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/admission-response.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/admission-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/review-approve-request.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/review-request.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/review-override-request.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/review-request.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/review-re_evaluate-request.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/review-request.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/review-response.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/review-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/lineage-root.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/lineage.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/lineage-child.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/lineage.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/tenant-unavailable.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/webhook-ingest-error-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/polling/pending.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/polling-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/polling/running.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/polling-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/polling/completed.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/polling-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/polling/review_required.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/polling-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/polling/failed.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/polling-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/polling/superseded.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/polling-response.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/webhook/completed.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/terminal-event.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/webhook/review_required.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/terminal-event.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/webhook/worker_interrupted.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/terminal-event.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/webhook/reviewed_approve.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/reviewed-event.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/webhook/reviewed_override.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/reviewed-event.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "fixtures/webhook/reviewed_reevaluate.json",
|
|
||||||
"role": "provider_consumer",
|
|
||||||
"schema": "schemas/reviewed-event.schema.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"provider_validation": "validate_fixture_corpus"
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
{
|
|
||||||
"rule_overrides": [],
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"snapshot": {
|
|
||||||
"captured_at": "2026-08-27T12:00:00Z",
|
|
||||||
"conversations": [
|
|
||||||
{
|
|
||||||
"assigned_agent_id": "agent-10",
|
|
||||||
"channel": "chat",
|
|
||||||
"conversation_id": "conversation-1001",
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"content": "Interested in a product demonstration.",
|
|
||||||
"message_id": "message-1001",
|
|
||||||
"sender_type": "contact",
|
|
||||||
"sent_at": "2026-08-27T11:50:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "I can arrange that for you.",
|
|
||||||
"message_id": "message-1002",
|
|
||||||
"sender_type": "agent",
|
|
||||||
"sent_at": "2026-08-27T11:55:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hitl_policy": {
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"policy_version": "1.0.0"
|
|
||||||
},
|
|
||||||
"identity_context": {
|
|
||||||
"contact_id": "contact-1001",
|
|
||||||
"details": {
|
|
||||||
"source": "ems"
|
|
||||||
},
|
|
||||||
"match_quality": "direct"
|
|
||||||
},
|
|
||||||
"lead_profile": {
|
|
||||||
"custom_fields": {
|
|
||||||
"industry": "software"
|
|
||||||
},
|
|
||||||
"email": "lead@example.test",
|
|
||||||
"ems_lead_id": "ems-lead-1001",
|
|
||||||
"name": "Example Lead",
|
|
||||||
"phone": "+84000000000",
|
|
||||||
"status": "open"
|
|
||||||
},
|
|
||||||
"scope": "linked_contact_history",
|
|
||||||
"trigger_metadata": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"trigger_conversation_id": "conversation-1001",
|
|
||||||
"trigger_type": "manual_omni"
|
|
||||||
},
|
|
||||||
"truncation": {
|
|
||||||
"dropped_conversations_count": 0,
|
|
||||||
"dropped_messages_count": 0,
|
|
||||||
"reason": null,
|
|
||||||
"truncated": false
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"subject_ref": "ems-lead-1001",
|
|
||||||
"tenant_id": "omni-account-42"
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"status": "pending",
|
|
||||||
"status_url": "/v1/scoring-runs/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"subject_ref": "ems-lead-1001",
|
|
||||||
"tenant_id": "omni-account-42"
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"detail": "Request validation failed",
|
|
||||||
"error_code": "validation_error"
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"parent_run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"relationship": "re_evaluation_child",
|
|
||||||
"run_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"parent_run_id": null,
|
|
||||||
"relationship": "root",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "deleted-block-followed-by-append",
|
|
||||||
"current_description": "Lead owner: Nguyễn Ánh 😊\n\nThe previous score block was removed.",
|
|
||||||
"expected_description": "Lead owner: Nguyễn Ánh 😊\n\nThe previous score block was removed.\n\n<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 61\nConfidence: 0.85\nMode: medium_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->",
|
|
||||||
"expected_error": null,
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": true
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "empty-description-append",
|
|
||||||
"current_description": "",
|
|
||||||
"expected_description": "<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 61\nConfidence: 0.85\nMode: medium_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->",
|
|
||||||
"expected_error": null,
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": true
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "failed-run-no-render",
|
|
||||||
"current_description": "Customer note: do not write a failure marker.",
|
|
||||||
"expected_description": "Customer note: do not write a failure marker.",
|
|
||||||
"expected_error": null,
|
|
||||||
"marker_input": null,
|
|
||||||
"run_status": "failed",
|
|
||||||
"writeback": false
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "multiple-complete-blocks",
|
|
||||||
"current_description": "<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 42\nConfidence: 0.71\nMode: light_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->\n\nUser text\n\n<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 42\nConfidence: 0.71\nMode: light_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->",
|
|
||||||
"expected_description": null,
|
|
||||||
"expected_error": "marker_block_malformed",
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": false
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "nested-block",
|
|
||||||
"current_description": "<!-- DOTB_LEAD_SCORING_START -->\nouter\n<!-- DOTB_LEAD_SCORING_START -->\ninner\n<!-- DOTB_LEAD_SCORING_END -->\n<!-- DOTB_LEAD_SCORING_END -->",
|
|
||||||
"expected_description": null,
|
|
||||||
"expected_error": "marker_block_malformed",
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": false
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "orphan-closing",
|
|
||||||
"current_description": "Notes before\n\npartial marker\n<!-- DOTB_LEAD_SCORING_END -->",
|
|
||||||
"expected_description": null,
|
|
||||||
"expected_error": "marker_block_malformed",
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": false
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "orphan-opening",
|
|
||||||
"current_description": "Notes before\n\n<!-- DOTB_LEAD_SCORING_START -->\npartial marker",
|
|
||||||
"expected_description": null,
|
|
||||||
"expected_error": "marker_block_malformed",
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": false
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "overlapping-block",
|
|
||||||
"current_description": "<!-- DOTB_LEAD_SCORING_START -->\nfirst\n<!-- DOTB_LEAD_SCORING_END --><!-- DOTB_LEAD_SCORING_END -->",
|
|
||||||
"expected_description": null,
|
|
||||||
"expected_error": "marker_block_malformed",
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": false
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "plain-description-append",
|
|
||||||
"current_description": "Existing customer note.",
|
|
||||||
"expected_description": "Existing customer note.\n\n<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 61\nConfidence: 0.85\nMode: medium_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->",
|
|
||||||
"expected_error": null,
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": true
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "single-valid-block-replace",
|
|
||||||
"current_description": "Customer note:\nKeep this line.\n\n<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 42\nConfidence: 0.71\nMode: light_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->\n\nFollow up tomorrow.",
|
|
||||||
"expected_description": "Customer note:\nKeep this line.\n\n<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 61\nConfidence: 0.85\nMode: medium_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->\n\nFollow up tomorrow.",
|
|
||||||
"expected_error": null,
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.85,
|
|
||||||
"degraded": false,
|
|
||||||
"final_score": 61,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai"
|
|
||||||
},
|
|
||||||
"run_status": "completed",
|
|
||||||
"writeback": true
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"case": "unicode-multiline-preservation",
|
|
||||||
"current_description": "Khách hàng: Nguyễn Ánh 😊\nDòng thứ hai.\n\n<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 42\nConfidence: 0.71\nMode: light_ai\nDegraded: no\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->\n\nGhi chú cuối: giữ nguyên nội dung người dùng.",
|
|
||||||
"expected_description": "Khách hàng: Nguyễn Ánh 😊\nDòng thứ hai.\n\n<!-- DOTB_LEAD_SCORING_START -->\nLead Scoring\nScore: 50\nConfidence: 0.62\nMode: rules_only_fallback\nDegraded: yes\nConfig: 1.0.0\nRun: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\nRun created: 2026-08-27T12:00:00Z\n<!-- DOTB_LEAD_SCORING_END -->\n\nGhi chú cuối: giữ nguyên nội dung người dùng.",
|
|
||||||
"expected_error": null,
|
|
||||||
"marker_input": {
|
|
||||||
"confidence": 0.62,
|
|
||||||
"degraded": true,
|
|
||||||
"final_score": 50,
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "rules_only_fallback"
|
|
||||||
},
|
|
||||||
"run_status": "review_required",
|
|
||||||
"writeback": true
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
{
|
|
||||||
"rule_overrides": [],
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"snapshot": {
|
|
||||||
"captured_at": "2026-08-27T12:00:00Z",
|
|
||||||
"conversations": [
|
|
||||||
{
|
|
||||||
"assigned_agent_id": "agent-10",
|
|
||||||
"channel": "chat",
|
|
||||||
"conversation_id": "conversation-1001",
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"content": "Interested in a product demonstration.",
|
|
||||||
"message_id": "message-1001",
|
|
||||||
"sender_type": "contact",
|
|
||||||
"sent_at": "2026-08-27T11:50:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "I can arrange that for you.",
|
|
||||||
"message_id": "message-1002",
|
|
||||||
"sender_type": "agent",
|
|
||||||
"sent_at": "2026-08-27T11:55:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hitl_policy": {
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"policy_version": "1.0.0"
|
|
||||||
},
|
|
||||||
"identity_context": {
|
|
||||||
"contact_id": "contact-1001",
|
|
||||||
"details": {
|
|
||||||
"source": "ems"
|
|
||||||
},
|
|
||||||
"match_quality": "direct"
|
|
||||||
},
|
|
||||||
"lead_profile": {
|
|
||||||
"custom_fields": {
|
|
||||||
"industry": "software"
|
|
||||||
},
|
|
||||||
"email": "lead@example.test",
|
|
||||||
"ems_lead_id": "ems-lead-1001",
|
|
||||||
"name": "Example Lead",
|
|
||||||
"phone": "+84000000000",
|
|
||||||
"status": "open"
|
|
||||||
},
|
|
||||||
"scope": "linked_contact_history",
|
|
||||||
"trigger_metadata": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"trigger_conversation_id": "conversation-1001",
|
|
||||||
"trigger_type": "manual_omni"
|
|
||||||
},
|
|
||||||
"truncation": {
|
|
||||||
"dropped_conversations_count": 0,
|
|
||||||
"dropped_messages_count": 0,
|
|
||||||
"reason": null,
|
|
||||||
"truncated": false
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"subject_ref": "ems-lead-other",
|
|
||||||
"tenant_id": "omni-account-42"
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"data": {
|
|
||||||
"confidence": 0.91,
|
|
||||||
"rule_score": 82,
|
|
||||||
"status": "completed"
|
|
||||||
},
|
|
||||||
"event_id": "11111111-1111-4111-8111-111111111111",
|
|
||||||
"event_type": "lead_scoring.run.terminal",
|
|
||||||
"occurred_at": "2026-08-27T12:00:03Z",
|
|
||||||
"run_created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"run_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"subject_ref": "ems-lead-1001",
|
|
||||||
"tenant_id": "omni-account-42"
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"decision": "override",
|
|
||||||
"expected_run_version": 1,
|
|
||||||
"note": "Reviewed against the latest customer context.",
|
|
||||||
"override_score": 101,
|
|
||||||
"reviewer": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"current_assignee_id": "agent-10",
|
|
||||||
"role": "agent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"captured_at": "2026-08-27T12:00:00Z",
|
|
||||||
"conversations": [
|
|
||||||
{
|
|
||||||
"conversation_id": "conversation-1001",
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"content": "This message is after capture.",
|
|
||||||
"message_id": "message-future",
|
|
||||||
"sender_type": "contact",
|
|
||||||
"sent_at": "2026-08-27T12:01:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hitl_policy": {
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"policy_version": "1.0.0"
|
|
||||||
},
|
|
||||||
"identity_context": {
|
|
||||||
"contact_id": "contact-1001",
|
|
||||||
"details": {
|
|
||||||
"source": "ems"
|
|
||||||
},
|
|
||||||
"match_quality": "direct"
|
|
||||||
},
|
|
||||||
"lead_profile": {
|
|
||||||
"custom_fields": {
|
|
||||||
"industry": "software"
|
|
||||||
},
|
|
||||||
"email": "lead@example.test",
|
|
||||||
"ems_lead_id": "ems-lead-1001",
|
|
||||||
"name": "Example Lead",
|
|
||||||
"phone": "+84000000000",
|
|
||||||
"status": "open"
|
|
||||||
},
|
|
||||||
"scope": "linked_contact_history",
|
|
||||||
"trigger_metadata": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"trigger_conversation_id": "conversation-1001",
|
|
||||||
"trigger_type": "manual_omni"
|
|
||||||
},
|
|
||||||
"truncation": {
|
|
||||||
"dropped_conversations_count": 0,
|
|
||||||
"dropped_messages_count": 0,
|
|
||||||
"reason": null,
|
|
||||||
"truncated": false
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"ai_metadata": {
|
|
||||||
"effort_profile": "medium",
|
|
||||||
"model": "openai/gpt-4o-mini",
|
|
||||||
"pass_count": 2
|
|
||||||
},
|
|
||||||
"ai_score": 78,
|
|
||||||
"breakdown": null,
|
|
||||||
"category": "hot",
|
|
||||||
"completed_at": "2026-08-27T12:00:03Z",
|
|
||||||
"confidence": 0.91,
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"degraded": false,
|
|
||||||
"effective_config": null,
|
|
||||||
"effective_score": 80,
|
|
||||||
"error_code": null,
|
|
||||||
"explanation": "Lead \u0111\u1ea1t 80/100 sau khi k\u1ebft h\u1ee3p b\u1ed9 quy t\u1eafc v\u00e0 ph\u00e2n t\u00edch h\u1ed9i tho\u1ea1i b\u1eb1ng AI.",
|
|
||||||
"final_score": 80,
|
|
||||||
"parent_run_id": null,
|
|
||||||
"recommended_action": "Li\u00ean h\u1ec7 v\u1edbi Lead trong 24 gi\u1edd \u0111\u1ec3 x\u00e1c nh\u1eadn b\u01b0\u1edbc \u0111\u0103ng k\u00fd ti\u1ebfp theo.",
|
|
||||||
"retryable": null,
|
|
||||||
"rule_score": 82,
|
|
||||||
"run_id": "33333333-3333-4333-8333-333333333333",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": "medium_ai",
|
|
||||||
"started_at": "2026-08-27T12:00:01Z",
|
|
||||||
"status": "completed",
|
|
||||||
"subject_ref": "ems-lead-1003",
|
|
||||||
"tenant_id": "omni-account-42",
|
|
||||||
"updated_at": "2026-08-27T12:00:03Z",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"ai_metadata": null,
|
|
||||||
"ai_score": null,
|
|
||||||
"breakdown": null,
|
|
||||||
"category": "cold",
|
|
||||||
"completed_at": "2026-08-27T12:07:01Z",
|
|
||||||
"confidence": null,
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"created_at": "2026-08-27T12:02:00Z",
|
|
||||||
"degraded": null,
|
|
||||||
"effective_config": null,
|
|
||||||
"effective_score": null,
|
|
||||||
"error_code": "worker_interrupted",
|
|
||||||
"explanation": "Kh\u00f4ng th\u1ec3 ch\u1ea5m \u0111i\u1ec3m.",
|
|
||||||
"final_score": null,
|
|
||||||
"parent_run_id": null,
|
|
||||||
"recommended_action": "Ki\u1ec3m tra l\u1ed7i v\u00e0 th\u1eed ch\u1ea5m \u0111i\u1ec3m l\u1ea1i.",
|
|
||||||
"retryable": true,
|
|
||||||
"rule_score": null,
|
|
||||||
"run_id": "55555555-5555-4555-8555-555555555555",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": null,
|
|
||||||
"started_at": "2026-08-27T12:02:01Z",
|
|
||||||
"status": "failed",
|
|
||||||
"subject_ref": "ems-lead-1005",
|
|
||||||
"tenant_id": "omni-account-42",
|
|
||||||
"updated_at": "2026-08-27T12:07:01Z",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"ai_metadata": null,
|
|
||||||
"ai_score": null,
|
|
||||||
"breakdown": null,
|
|
||||||
"category": null,
|
|
||||||
"completed_at": null,
|
|
||||||
"confidence": null,
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"degraded": null,
|
|
||||||
"effective_config": null,
|
|
||||||
"effective_score": null,
|
|
||||||
"error_code": null,
|
|
||||||
"explanation": null,
|
|
||||||
"final_score": null,
|
|
||||||
"parent_run_id": null,
|
|
||||||
"recommended_action": null,
|
|
||||||
"retryable": null,
|
|
||||||
"rule_score": null,
|
|
||||||
"run_id": "11111111-1111-4111-8111-111111111111",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": null,
|
|
||||||
"started_at": null,
|
|
||||||
"status": "pending",
|
|
||||||
"subject_ref": "ems-lead-1001",
|
|
||||||
"tenant_id": "omni-account-42",
|
|
||||||
"updated_at": "2026-08-27T12:00:00Z",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"ai_metadata": null,
|
|
||||||
"ai_score": null,
|
|
||||||
"breakdown": null,
|
|
||||||
"category": null,
|
|
||||||
"completed_at": null,
|
|
||||||
"confidence": null,
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"created_at": "2026-08-27T12:00:00Z",
|
|
||||||
"degraded": null,
|
|
||||||
"effective_config": null,
|
|
||||||
"effective_score": null,
|
|
||||||
"error_code": null,
|
|
||||||
"explanation": null,
|
|
||||||
"final_score": null,
|
|
||||||
"parent_run_id": null,
|
|
||||||
"recommended_action": null,
|
|
||||||
"retryable": null,
|
|
||||||
"rule_score": null,
|
|
||||||
"run_id": "22222222-2222-4222-8222-222222222222",
|
|
||||||
"scoring_config_version": "1.0.0",
|
|
||||||
"scoring_mode": null,
|
|
||||||
"started_at": "2026-08-27T12:00:01Z",
|
|
||||||
"status": "running",
|
|
||||||
"subject_ref": "ems-lead-1002",
|
|
||||||
"tenant_id": "omni-account-42",
|
|
||||||
"updated_at": "2026-08-27T12:00:01Z",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"decision": "approve",
|
|
||||||
"expected_run_version": 1,
|
|
||||||
"note": "Reviewed against the latest customer context.",
|
|
||||||
"reviewer": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"current_assignee_id": "agent-10",
|
|
||||||
"role": "agent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"decision": "override",
|
|
||||||
"expected_run_version": 1,
|
|
||||||
"note": "Reviewed against the latest customer context.",
|
|
||||||
"override_score": 90,
|
|
||||||
"reviewer": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"current_assignee_id": "agent-10",
|
|
||||||
"role": "agent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"decision": "re_evaluate",
|
|
||||||
"expected_run_version": 1,
|
|
||||||
"note": "Reviewed against the latest customer context.",
|
|
||||||
"reviewer": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"current_assignee_id": "agent-10",
|
|
||||||
"role": "agent"
|
|
||||||
},
|
|
||||||
"snapshot": {
|
|
||||||
"captured_at": "2026-08-27T12:05:00Z",
|
|
||||||
"conversations": [
|
|
||||||
{
|
|
||||||
"assigned_agent_id": "agent-10",
|
|
||||||
"channel": "chat",
|
|
||||||
"conversation_id": "conversation-1001",
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"content": "Interested in a product demonstration.",
|
|
||||||
"message_id": "message-1001",
|
|
||||||
"sender_type": "contact",
|
|
||||||
"sent_at": "2026-08-27T11:55:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "I can arrange that for you.",
|
|
||||||
"message_id": "message-1002",
|
|
||||||
"sender_type": "agent",
|
|
||||||
"sent_at": "2026-08-27T12:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hitl_policy": {
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"policy_version": "1.0.0"
|
|
||||||
},
|
|
||||||
"identity_context": {
|
|
||||||
"contact_id": "contact-1001",
|
|
||||||
"details": {
|
|
||||||
"source": "ems"
|
|
||||||
},
|
|
||||||
"match_quality": "direct"
|
|
||||||
},
|
|
||||||
"lead_profile": {
|
|
||||||
"custom_fields": {
|
|
||||||
"industry": "software"
|
|
||||||
},
|
|
||||||
"email": "lead@example.test",
|
|
||||||
"ems_lead_id": "ems-lead-1001",
|
|
||||||
"name": "Example Lead",
|
|
||||||
"phone": "+84000000000",
|
|
||||||
"status": "open"
|
|
||||||
},
|
|
||||||
"scope": "linked_contact_history",
|
|
||||||
"trigger_metadata": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"trigger_conversation_id": "conversation-1001",
|
|
||||||
"trigger_type": "manual_omni"
|
|
||||||
},
|
|
||||||
"truncation": {
|
|
||||||
"dropped_conversations_count": 0,
|
|
||||||
"dropped_messages_count": 0,
|
|
||||||
"reason": null,
|
|
||||||
"truncated": false
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"calculated_score": 61,
|
|
||||||
"child_run_id": null,
|
|
||||||
"decision": "approve",
|
|
||||||
"effective_score": 61,
|
|
||||||
"note": null,
|
|
||||||
"reviewed_at": "2026-08-27T12:05:00Z",
|
|
||||||
"reviewed_by": "agent-10",
|
|
||||||
"run_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
||||||
"status": "completed",
|
|
||||||
"subject_ref": "ems-lead-1002",
|
|
||||||
"tenant_id": "omni-account-42",
|
|
||||||
"version": 2
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"age_primary_max": 15,
|
|
||||||
"age_primary_min": 3,
|
|
||||||
"age_primary_points": 40,
|
|
||||||
"age_secondary_max": 18,
|
|
||||||
"age_secondary_min": 16,
|
|
||||||
"age_secondary_points": 20,
|
|
||||||
"ai_model": "openai/gpt-4o-mini",
|
|
||||||
"ai_timeout_seconds": 10.0,
|
|
||||||
"ai_weight": 0.6,
|
|
||||||
"base_score": 50,
|
|
||||||
"completeness_threshold": 0.5,
|
|
||||||
"confidence": 0.85,
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"dynamic_rules": [],
|
|
||||||
"education_demo_enabled": false,
|
|
||||||
"effort_profile": "medium",
|
|
||||||
"fallback_policy": "rules_only_review",
|
|
||||||
"hitl_policy_version": "1.0.0",
|
|
||||||
"medium_extract_prompt": "Bạn là trợ lý AI phân tích hội thoại để đánh giá tiềm năng học viên. Hãy trích xuất bằng chứng có cấu trúc về nhu cầu khóa học, mức độ quan tâm, thời điểm đăng ký và rào cản. Trả lời bằng tiếng Việt và không tự bịa dữ kiện.",
|
|
||||||
"medium_score_prompt": "Bạn là chuyên gia chấm điểm Lead giáo dục từ 0 đến 100. Dựa trên bằng chứng đã trích xuất, hãy đánh giá khả năng đăng ký, độ tin cậy, lý do và các yếu tố tích cực hoặc tiêu cực. Lưu ý: Không cộng lại điểm các customer rules hoặc metrics đã tính ở phần quy tắc riêng. Trả lời bằng tiếng Việt và không tự bịa dữ kiện.",
|
|
||||||
"recency_cold_max_days": 30.0,
|
|
||||||
"recency_cold_points": 5,
|
|
||||||
"recency_cool_max_days": 14.0,
|
|
||||||
"recency_cool_points": 15,
|
|
||||||
"recency_hot_max_days": 3.0,
|
|
||||||
"recency_hot_points": 40,
|
|
||||||
"recency_warm_max_days": 7.0,
|
|
||||||
"recency_warm_points": 25,
|
|
||||||
"rule_weight": 0.4,
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"metric": "profile_field_count",
|
|
||||||
"minimum": 2,
|
|
||||||
"points": 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"metric": "conversation_count",
|
|
||||||
"minimum": 1,
|
|
||||||
"points": 10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"selected_behavioral_metrics": [
|
|
||||||
"message_count",
|
|
||||||
"conversation_count",
|
|
||||||
"conversation_turn_count",
|
|
||||||
"customer_message_count",
|
|
||||||
"customer_initiated_return"
|
|
||||||
],
|
|
||||||
"source_call_in_points": 15,
|
|
||||||
"source_campaign_points": 8,
|
|
||||||
"source_digital_points": 10,
|
|
||||||
"source_other_points": 5,
|
|
||||||
"source_referral_points": 20,
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"captured_at": "2026-08-27T12:00:00Z",
|
|
||||||
"conversations": [
|
|
||||||
{
|
|
||||||
"assigned_agent_id": "agent-10",
|
|
||||||
"channel": "chat",
|
|
||||||
"conversation_id": "conversation-1001",
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"content": "Interested in a product demonstration.",
|
|
||||||
"message_id": "message-1001",
|
|
||||||
"sender_type": "contact",
|
|
||||||
"sent_at": "2026-08-27T11:50:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": "I can arrange that for you.",
|
|
||||||
"message_id": "message-1002",
|
|
||||||
"sender_type": "agent",
|
|
||||||
"sent_at": "2026-08-27T11:55:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hitl_policy": {
|
|
||||||
"confidence_threshold": 0.8,
|
|
||||||
"policy_version": "1.0.0"
|
|
||||||
},
|
|
||||||
"identity_context": {
|
|
||||||
"contact_id": "contact-1001",
|
|
||||||
"details": {
|
|
||||||
"source": "ems"
|
|
||||||
},
|
|
||||||
"match_quality": "direct"
|
|
||||||
},
|
|
||||||
"lead_profile": {
|
|
||||||
"custom_fields": {
|
|
||||||
"industry": "software"
|
|
||||||
},
|
|
||||||
"email": "lead@example.test",
|
|
||||||
"ems_lead_id": "ems-lead-1001",
|
|
||||||
"name": "Example Lead",
|
|
||||||
"phone": "+84000000000",
|
|
||||||
"status": "open"
|
|
||||||
},
|
|
||||||
"scope": "linked_contact_history",
|
|
||||||
"trigger_metadata": {
|
|
||||||
"actor_id": "agent-10",
|
|
||||||
"trigger_conversation_id": "conversation-1001",
|
|
||||||
"trigger_type": "manual_omni"
|
|
||||||
},
|
|
||||||
"truncation": {
|
|
||||||
"dropped_conversations_count": 0,
|
|
||||||
"dropped_messages_count": 0,
|
|
||||||
"reason": null,
|
|
||||||
"truncated": false
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"detail": "Webhook destination is not available",
|
|
||||||
"error_code": "subscriber_tenant_unavailable"
|
|
||||||
}
|
|
||||||