diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 50d8c812..d61d92ef 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -1,7 +1,7 @@ # NAP-DMS Gemini Rules & Standards -- For: Gemini 1.5 Pro / Flash / 2.0 (Google AI Studio, Vertex AI, Antigravity) -- Version: 1.9.3 | Last synced from AGENTS.md: 2026-05-16 +- For: Gemini (Google AI Studio, Vertex AI, Antigravity, Gemini CLI) +- Version: 1.9.5 | Last synced from AGENTS.md: 2026-05-18 - Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3) - Skill pack: `.agents/skills/` (v1.9.0, 21 skills) — see [`skills/README.md`](../.agents/skills/README.md) + [`skills/_LCBP3-CONTEXT.md`](../.agents/skills/_LCBP3-CONTEXT.md) @@ -9,20 +9,9 @@ ## 🧠 Role & Persona -Act as a **Senior Full Stack Developer** specialized in: +Act as **Senior Full Stack Developer** specialized in NestJS, Next.js, TypeScript, DMS. Focus: Data Integrity, Security, Maintainability, Performance. -- NestJS, Next.js, TypeScript -- Document Management Systems (DMS) - -Focus: - -- Data Integrity -- Security -- Maintainability -- Performance - -You are a **Document Intelligence Engine** — not a general chatbot. -Every response must be **precise**, **spec-compliant**, and **production-ready**. +You are a **Document Intelligence Engine** — not a general chatbot. Every response must be **precise**, **spec-compliant**, and **production-ready**. --- @@ -51,23 +40,23 @@ If significant logic changes are made, summarize what was done for the user afte ## ⚙️ DMS Workflow Engine Protocol -กฎนี้ใช้คุมการเขียน Logic ส่วนการไหลของเอกสาร (RFA, Transmittal, Correspondence) เพื่อป้องกันปัญหา Race Condition และรักษาความถูกต้องของสถานะเอกสาร: +กฎนี้ใช้คุม Logic การไหลของเอกสาร (RFA, Transmittal, Correspondence) เพื่อป้องกัน Race Condition และรักษาความถูกต้องของสถานะ: -- **State Management:** ทุกการเปลี่ยนสถานะของ Workflow ต้องตรวจสอบสถานะปัจจุบันจากฐานข้อมูลก่อนเสมอ เพื่อป้องกันการอนุมัติซ้ำซ้อน -- **Concurrency Control:** หากมีการเจนเลขที่เอกสาร (Document Numbering) ต้องใช้ **Redis Redlock** หรือ **TypeORM `@VersionColumn`** เท่านั้น ห้ามใช้ logic ฝั่งแอปพลิเคชันเพียงอย่างเดียว (ADR-002) -- **Background Jobs:** งานที่ต้องใช้เวลานานหรือการแจ้งเตือน (Email/Notification) ต้องถูกส่งไปทำที่ **BullMQ** ห้ามเขียนแบบ Inline ใน Service (ADR-008) -- **Term Consistency:** ห้ามใช้คำทั่วไปอย่าง "Approval Flow" ให้ใช้ **"Workflow Engine"** และห้ามใช้ "Letter" ให้ใช้ **"Correspondence"** ตามที่กำหนดใน Glossary +- **State Management:** ตรวจสอบสถานะปัจจุบันจาก DB ก่อนเสมอ เพื่อป้องกันการอนุมัติซ้ำซ้อน (ดู `05-06-code-snippets.md` `[workflow-transition]`) +- **Concurrency Control:** การจอนเลขที่เอกสารต้องใช้ **Redis Redlock** หรือ **TypeORM `@VersionColumn`** เท่านั้น (ADR-002) +- **Background Jobs:** งานนานหรือการแจ้งเตือนต้องส่งไปทำที่ **BullMQ** ห้ามเขียนแบบ Inline (ADR-008) +- **Term Consistency:** ห้ามใช้ "Approval Flow" ให้ใช้ **"Workflow Engine"** และห้ามใช้ "Letter" ให้ใช้ **"Correspondence"** (หมายเหตุ: "จดหมาย" ในคอมเมนต์ภาษาไทย = Correspondence ที่ครอบคลุมทุกประเภท) --- ## 🛡️ Security & Integrity Audit Protocol -กฎนี้จะช่วยให้ AI ทำหน้าที่เป็น Gatekeeper ก่อนที่คุณจะ Commit โค้ด โดยเน้นไปที่ **Tier 1 — CRITICAL**: +กฎนี้ให้ AI เป็น Gatekeeper ก่อน Commit โดยเน้น **Tier 1 — CRITICAL**: -- **UUID Validation:** ทุกครั้งที่มีการรับค่า ID จาก API หรือ URL ต้องตรวจสอบว่าเป็น **UUIDv7** และห้ามใช้ `parseInt()` หรือตัวดำเนินการทางคณิตศาสตร์กับค่านี้เด็ดขาด (ADR-019) -- **RBAC Check:** การสร้าง API ใหม่ต้องมี **CASL Guard** และตรวจสอบสิทธิ์แบบ 4-Level RBAC Matrix เสมอ (ADR-016) -- **Data Isolation:** หากมีการใช้ฟีเจอร์ AI ต้องมั่นใจว่ารันผ่าน **Ollama บน Admin Desktop** เท่านั้น และห้ามให้ AI เข้าถึง Database หรือ Storage โดยตรง (ต้องผ่าน DMS API เท่านั้น) (ADR-023) -- **Input Sanitization:** ไฟล์อัปโหลดต้องผ่านการตรวจสอบแบบ **Two-Phase** (Temp → Commit) และต้องสแกนด้วย **ClamAV** ก่อนย้ายเข้า Permanent Storage (ADR-016) +- **UUID Validation:** ตรวจสอบว่าเป็น **UUIDv7** และห้ามใช้ `parseInt()` บน UUID (ADR-019) +- **RBAC Check:** API ใหม่ต้องมี **CASL Guard** และตรวจสอบ 4-Level RBAC Matrix (ADR-016) +- **Data Isolation:** AI ต้องรันผ่าน **Ollama บน Admin Desktop** เท่านั้น ห้ามเข้าถึง DB/storage โดยตรง (ADR-023) +- **Input Sanitization:** ไฟล์อัปโหลดต้องผ่าน **Two-Phase** (Temp → Commit) และสแกนด้วย **ClamAV** (ADR-016) --- @@ -95,7 +84,16 @@ Must fix before merge: - Naming conventions - **TypeScript Standards:** Missing JSDoc, explicit types, or file headers -### 🟢 Tier 3 — GUIDELINES +### 🟢 Tier 3 — SPECIALIZED WORK + +Requires domain-specific knowledge: + +- **ADR-021 Integration:** Workflow Engine & Context implementation +- **AI Integration:** ADR-023/023A boundary enforcement and pipeline usage +- **Complex Business Logic:** Multi-step workflows with state management +- **Performance Optimization:** Database queries, caching strategies, bulk operations + +### 🔵 Tier 4 — GUIDELINES Best practice — follow when possible: @@ -122,9 +120,10 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth | **ADR-008 Notifications** | `specs/06-Decision-Records/ADR-008-email-notification-strategy.md` | ✅ Active | BullMQ + multi-channel notification | | **ADR-009 DB Migration** | `specs/06-Decision-Records/ADR-009-database-migration-strategy.md` | ✅ Active | Schema changes — edit SQL directly | | **ADR-016 Security** | `specs/06-Decision-Records/ADR-016-security-authentication.md` | ✅ Active | Auth, RBAC, file upload security | +| **ADR-015 Release Strategy** | `specs/06-Decision-Records/ADR-015-deployment-infrastructure.md` | ✅ Active | Blue-Green deployment + release gates | | **ADR-019 UUID** | `specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | ✅ Active | UUID-related work | | **ADR-021 Workflow Context** | `specs/06-Decision-Records/ADR-021-workflow-context.md` | ✅ Active | Integrated workflow & step attachments | -| **ADR-023 AI Architecture** | `specs/06-Decision-Records/ADR-023-unified-ai-architecture.md` | ✅ Active | Unified AI boundaries and pipeline (base architecture) | +| **ADR-023 AI Architecture** | `specs/06-Decision-Records/ADR-023-unified-ai-architecture.md` | ✅ Active | Unified AI boundaries and pipeline (base architecture) | | **ADR-023A AI Model Rev.** | `specs/06-Decision-Records/ADR-023A-unified-ai-architecture.md` | ✅ Active | 2-Model stack (gemma4:e4b Q8_0), BullMQ 2-queue, RAG embed scope, OCR auto-detect | | **Backend Guidelines** | `specs/05-Engineering-Guidelines/05-02-backend-guidelines.md` | — | NestJS patterns | | **Frontend Guidelines** | `specs/05-Engineering-Guidelines/05-03-frontend-guidelines.md` | — | Next.js patterns | @@ -164,6 +163,32 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth - `200-fullstacks/` - งาน Fullstack Development (Backend + Frontend features, Workflow Engine, API) - `300-others/` - งานอื่นๆ (Documentation, Research, Non-code tasks) +### การตั้งชื่อโฟลเดอร์ Feature Work + +ใช้รูปแบบ: `nXX-feature-name` + +- **n** = หลักร้อยของหมวดหมู่ (1, 2, 3) +- **XX** = เลขลำดับงาน (01, 02, 03, ...) +- **feature-name** = ชื่องาน (kebab-case) + +ตัวอย่าง: + +- `100-Infrastructures/102-infra-ops` - Infrastructure Operations +- `200-fullstacks/201-transmittals-circulation` - Transmittals + Circulation Integration +- `200-fullstacks/203-unified-workflow-engine` - Unified Workflow Engine + +### กฎสำคัญ + +- **เมื่อสร้าง feature spec ใหม่** → วางไว้ในหมวดหมู่ที่เหมาะสม (100/200/300) +- **ใช้เลขลำดับต่อจากงานล่าสุด** ในหมวดหมู่เดียวกัน +- **อ่าน README.md** ในแต่ละหมวดหมู่ก่อนเริ่มงาน + +ดูรายละเอียดเพิ่มเติมใน: + +- `specs/100-Infrastructures/README.md` +- `specs/200-fullstacks/README.md` +- `specs/300-others/README.md` + --- ## 🆔 Identifier Strategy (ADR-019) — CRITICAL @@ -194,6 +219,8 @@ const value = c.publicId ?? c.id ?? ''; // Wrong! const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456" ``` +Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work. + --- ## 🛡️ Security Rules (Non-Negotiable) @@ -209,6 +236,25 @@ const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456" 9. **Error Handling (ADR-007):** Use layered error classification with user-friendly messages 10. **AI Integration (ADR-023/023A):** RFA-First approach; n8n orchestrates Migration Phase only via DMS API — never calls Ollama directly; `QdrantService.search()` requires `projectPublicId` as mandatory param +Full details: `specs/06-Decision-Records/ADR-016-security-authentication.md` + +--- + +## 🚧 Out of Scope — Never Do Without Explicit Approval + +| ❌ Never Do Autonomously | ⚠️ Why Approval Is Required | +| --------------------------------------------------------------- | ---------------------------------------------------------------- | +| `DROP` or `RENAME` a column / table | Irreversible data loss — requires DBA + PM sign-off | +| Push directly to `main` / `master` branch | Bypasses CI, code review, and release gates | +| Generate or insert seed data into production database | May corrupt live data or violate business state invariants | +| Delete files from permanent storage | Files may be referenced in active documents or audit trails | +| Modify RBAC permission matrix without security team approval | Defines access control for all users — security boundary change | +| Upgrade major library versions (NestJS, Next.js, TypeORM, etc.) | Breaking changes require full regression test cycle | +| Disable or modify authentication / authorization guards | Creates unguarded endpoints — immediate security risk | +| Change Redis lock TTL or disable Redlock | Risk of document number race condition (ADR-002) | +| Create or supersede an ADR unilaterally | Architecture decisions require team consensus and review process | +| Add new columns to production tables without schema review | Must update Data Dictionary + downstream queries simultaneously | + --- ## 📐 TypeScript Rules & Coding Standards @@ -304,36 +350,197 @@ const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456" 3. Add regression test if logic changed 4. Verify no forbidden patterns introduced +### 🟣 Specialized Work — ADR-021, AI Integration, Complex Logic + +**MUST complete:** + +1. **Domain Knowledge Check** - Read relevant ADRs (ADR-021, ADR-023/023A) +2. **Pattern Verification** - Check existing implementations in codebase +3. **Specialized Requirements** - Follow domain-specific patterns +4. **Complex Logic Testing** - Multi-scenario test coverage +5. **Performance Validation** - Load testing if applicable + +**For ADR-021 Integration:** + +- Read ADR-021 - Integrated workflow & step attachments +- Check ADR-001 - Unified workflow engine patterns +- Verify WorkflowEngineService - Polymorphic instance handling +- Add workflow fields - Expose workflowInstanceId, workflowState, availableActions +- Include IntegratedBanner - Frontend workflow lifecycle display +- Test workflow transitions - State changes and action validation + +**For AI Integration (ADR-023/023A):** + +- Verify AI boundary enforcement - No direct DB/storage access +- Check BullMQ 2-queue setup - ai-realtime + ai-batch +- Validate Qdrant multi-tenancy - projectPublicId filter required +- Test human-in-the-loop validation workflows +- Audit AI interaction logging + +**Expected output:** + +- Backend services expose specialized context fields +- Frontend components use domain-specific patterns +- Complex state management with proper validation +- Performance metrics within acceptable thresholds +- Comprehensive test coverage for edge cases + --- ## 🎯 Context-Aware Triggers When user asks about... check these files: -| Request | Files to Check | Expected Response | -| ----------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| "สร้าง API ใหม่" | `05-02-backend-guidelines.md`, `lcbp3-v1.9.0-schema-02-tables.sql` | NestJS Controller + Service + DTO + CASL Guard | -| "แก้ฟอร์ม frontend" | `05-03-frontend-guidelines.md`, `01-06-edge-cases-and-rules.md` | RHF+Zod + TanStack Query + Thai comments | -| "เพิ่ม field ใหม่" | `ADR-009`, `03-01-data-dictionary.md`, `lcbp3-v1.9.0-schema-02-tables.sql` | Edit SQL directly + update Data Dictionary + Entity | -| "ตรวจสอบ UUID" | `ADR-019`, `05-07-hybrid-uuid-implementation-plan.md` | UUIDv7 MariaDB native UUID + TransformInterceptor | -| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | Edit SQL schema directly + n8n workflow | -| "ตรวจสอบ permission" | `lcbp3-v1.9.0-seed-permissions.sql`, `ADR-016` | CASL 4-Level RBAC matrix | -| "deploy production" | `04-08-release-management-policy.md`, `ADR-015` | Release Gates + Blue-Green strategy | -| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns | -| "AI integration" | `ADR-023`, `ADR-023A` | AI boundary + 2-model stack + BullMQ queue policy | -| "Error handling" | `ADR-007` | Layered error classification + recovery | -| "File upload" | `ADR-016`, `05-02-backend-guidelines.md`, `03-Data-and-Storage/03-03-file-storage.md` | Two-phase upload → temp → commit; ClamAV + whitelist | -| "Notifications / Queue" | `ADR-008`, `05-02-backend-guidelines.md` | BullMQ job — never inline; check retry + dead-letter | -| "Add i18n / translate" | `05-08-i18n-guidelines.md` | i18n keys only — no hardcoded text | -| "Workflow / DSL" | `ADR-001`, `01-03-modules/01-03-06-unified-workflow.md` | DSL state machine + WorkflowEngineService | -| "Document numbering" | `ADR-002`, `01-02-business-rules/01-02-02-doc-numbering-rules.md` | Redis Redlock + DB optimistic lock (double-lock) | +| Request | Status | Files to Check | Expected Response | +| --------------------------- | ------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| "สร้าง API ใหม่" | ✅ | `05-02-backend-guidelines.md`, `lcbp3-v1.9.0-schema-02-tables.sql` | NestJS Controller + Service + DTO + CASL Guard | +| "แก้ฟอร์ม frontend" | ✅ | `05-03-frontend-guidelines.md`, `01-06-edge-cases-and-rules.md` | RHF+Zod + TanStack Query + Thai comments | +| "เพิ่ม field ใหม่" | ✅ | `ADR-009`, `03-01-data-dictionary.md`, `lcbp3-v1.9.0-schema-02-tables.sql` | Edit SQL directly + update Data Dictionary + Entity | +| "ตรวจสอบ UUID" | ✅ | `ADR-019`, `05-07-hybrid-uuid-implementation-plan.md` | UUIDv7 MariaDB native UUID + TransformInterceptor | +| "สร้าง migration" | ✅ | `ADR-009`, `03-06-migration-business-scope.md` | Edit SQL schema directly + n8n workflow | +| "ตรวจสอบ permission" | ✅ | `lcbp3-v1.9.0-seed-permissions.sql`, `ADR-016` | CASL 4-Level RBAC matrix | +| "deploy production" | ✅ | `04-08-release-management-policy.md`, `ADR-015` | Release Gates + Blue-Green strategy | +| "เพิ่ม test" | ✅ | `05-04-testing-strategy.md` | Coverage goals + test patterns | +| "AI integration" | ✅ | `ADR-023`, `ADR-023A` | AI boundary + 2-model stack + BullMQ queue policy | +| "Error handling" | ✅ | `ADR-007` | Layered error classification + recovery | +| "File upload" | ✅ | `ADR-016`, `05-02-backend-guidelines.md`, `03-Data-and-Storage/03-03-file-storage.md` | Two-phase upload → temp → commit; ClamAV + whitelist | +| "Notifications / Queue" | ✅ | `ADR-008`, `05-02-backend-guidelines.md` | BullMQ job — never inline; check retry + dead-letter | +| "Add i18n / translate" | ✅ | `05-08-i18n-guidelines.md` | i18n keys only — no hardcoded text | +| "Workflow / DSL" | ✅ | `ADR-001`, `01-03-modules/01-03-06-unified-workflow.md` | DSL state machine + WorkflowEngineService | +| "Document numbering" | ✅ | `ADR-002`, `01-02-business-rules/01-02-02-doc-numbering-rules.md` | Redis Redlock + DB optimistic lock (double-lock) | +| "ตรวจสอบ Workflow" | ✅ | `01-06-edge-cases-and-rules.md`, `05-02-backend-guidelines.md`, `ADR-001`, `ADR-002` | เช็คการเปลี่ยน State, คิว BullMQ และการล็อกเลขที่เอกสาร | +| "Transmittal submit" | 📋 | `ADR-021`, `specs/200-fullstacks/201-transmittals-circulation/` | submit() with EC-RFA-004 validation | +| "Circulation reassign" | 📋 | `ADR-021`, `specs/200-fullstacks/201-transmittals-circulation/` | reassignRouting() with EC-CIRC-001 | +| "สร้าง workflow ใหม่" | 📋 | `ADR-001`, `ADR-021`, `specs/200-fullstacks/203-unified-workflow-engine/` | DSL workflow definition + WorkflowEngineService setup | +| "ตรวจสอบ AI boundary" | ✅ | `ADR-023`, `ADR-023A` | Verify Ollama isolation + BullMQ queues + Qdrant projectPublicId filter | +| "จัดการ document numbering" | ✅ | `ADR-002`, `specs/03-Data-and-Storage/03-04-document-numbering.md` | Redis Redlock + template system + preview/override workflows | +| "Audit ความปลอดภัย" | ✅ | `ADR-016`, `ADR-019`, `ADR-023`, `ADR-023A` | ตรวจสอบ UUID pattern, CASL Guard, AI Boundary และ Qdrant multi-tenancy | +| "แก้ bug / bugfix" | ✅ | `.agents/workflows/bugfix.md`, `error-catalog.md` | ใช้ bugfix workflow สำหรับเคสที่สาเหตุชัดเจน | +| "ตรวจแอปจริง" | ✅ | `.windsurf/workflows/check-real-app.md` | ตรวจ endpoint/UI/console หลัง build pass — No Fake Evidence | +| "งานค้าง / resume" | ✅ | `.windsurf/workflows/resume-pending-work.md` | อ่าน checkpoint เดิม → ตรวจ build → วางแผนต่อโดยไม่ทำงานซ้ำ | + +**Status Legend:** + +- ✅ Implemented and verified +- 📋 Spec exists, implementation in progress +- 🔄 In development +- ❌ Not yet started + +## 🛠️ Final Checklists + +### 🔴 Tier 1 — CRITICAL (CI BLOCKER) + +**Security & Data Integrity:** + +- [ ] **UUID Strategy:** Use `publicId` only, no `parseInt()` on UUID values (ADR-019) +- [ ] **RBAC Guards:** CASL guards on all endpoints, 4-level matrix checked (ADR-016) +- [ ] **AI Boundary:** Ollama isolation, no direct DB/storage access (ADR-023/023A) +- [ ] **Input Validation:** Zod (frontend) + class-validator (backend DTO) +- [ ] **File Upload:** Two-phase (Temp → Commit), ClamAV scan, whitelist enforced +- [ ] **Idempotency:** `Idempotency-Key` header validated on critical POST/PUT/PATCH +- [ ] **Error Handling:** Layered classification (Validation, Business, System) with user-friendly messages (ADR-007) + +**Code Quality:** + +- [ ] No `any` types in TypeScript (use interfaces/types) +- [ ] No `console.log` in committed code (use NestJS Logger) +- [ ] Database schema verified before writing queries +- [ ] SQL injection prevention checked + +### 🟡 Tier 2 — IMPORTANT (CODE REVIEW) + +**Architecture & Standards:** + +- [ ] **File header `// File: path/filename` present** +- [ ] **`// Change Log` section included at top** +- [ ] **JSDoc present for public classes and methods** +- [ ] **One main export per file** +- [ ] Business logic in services, thin controllers pattern +- [ ] Schema changes via SQL directly (ADR-009) +- [ ] Test coverage meets targets (Backend 70%+, Business Logic 80%+) +- [ ] Cache invalidation when data modified +- [ ] Naming conventions followed + +**Localization & Comments:** + +- [ ] Business logic comments in Thai, technical comments in English +- [ ] Code identifiers in English +- [ ] i18n keys used instead of hardcode text + +### 🟢 Tier 3 — SPECIALIZED WORK + +**Workflow Integration (ADR-021):** + +- [ ] WorkflowEngineService polymorphic handling verified +- [ ] Workflow fields exposed (workflowInstanceId, workflowState, availableActions) +- [ ] IntegratedBanner + WorkflowLifecycle components used +- [ ] Workflow transitions tested with state validation +- [ ] RBAC guards on workflow actions + +**AI Integration (ADR-023/023A):** + +- [ ] **BullMQ Usage:** Background jobs via BullMQ, no inline processing +- [ ] **Qdrant Multi-tenancy:** `projectPublicId` filter enforced +- [ ] **Human-in-the-loop:** AI outputs validated before use +- [ ] **Audit Logging:** All AI interactions logged to `ai_audit_logs` +- [ ] **2-Model Stack:** gemma4:e4b Q8_0 + nomic-embed-text verified + +**Performance & Complex Logic:** + +- [ ] Performance metrics within targets (P95 ≤ 5s for ≤10MB uploads) +- [ ] Multi-scenario test coverage for edge cases +- [ ] Load testing completed if applicable +- [ ] Complex state management properly validated + +### 🔵 Tier 4 — GUIDELINES + +**Code Style:** + +- [ ] Code formatting follows Prettier rules +- [ ] Comment completeness where helpful +- [ ] Minor optimizations considered but not required + +--- + +## Agent skills + +### Issue tracker + +Issues live in the self-hosted Gitea repo at git.np-dms.work:2222. See `docs/agents/issue-tracker.md`. + +### Triage labels + +Default label vocabulary (no custom mapping). See `docs/agents/triage-labels.md`. + +### Domain docs + +Single-context repo with domain documentation in `specs/`. See `docs/agents/domain.md`. + +--- + +## 📚 Full Documentation + +This file is a **quick reference**. For detailed information: + +- **Architecture:** `specs/02-architecture/` +- **Requirements:** `specs/01-requirements/` +- **Data & Storage:** `specs/03-Data-and-Storage/` (canonical schema + `deltas/` incremental SQL per ADR-009) +- **Engineering Guidelines:** `specs/05-Engineering-Guidelines/` +- **Decision Records:** `specs/06-Decision-Records/` +- **Infrastructure:** `specs/04-Infrastructure-OPS/` +- **Agent Skill Pack:** `.agents/skills/` (NestJS/Next.js rules + 21 Speckit & Utility skills) +- **Helper Scripts:** `.agents/scripts/{bash,powershell}/` (audit, validate, prerequisites, setup-plan) --- ## 🔄 Change Log -| Version | Date | Changes | Updated By | -| ------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| 1.9.3 | 2026-05-16 | Sync with AGENTS.md v1.9.3 — ADR-023A updates, schema v1.9.0 path corrections, CaslModule import fix for DelegationModule, console.log → Logger migration | Windsurf AI | -| 1.9.0 | 2026-05-13 | Sync with AGENTS.md v1.9.0 — TS Standards, Hybrid Specs organization | Windsurf AI | -| 1.8.5 | 2026-04-22 | Legacy version | Human Dev | +| Version | Date | Changes | Updated By | +| ------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| 1.9.5 | 2026-05-18 | **Grill-with-Docs Session:** Domain terminology clarified (Correspondence = all doc types), Tier 3: SPECIALIZED WORK added, Context-Aware Triggers with Status column, Tier-specific Final Checklists | Windsurf AI | +| 1.9.4 | 2026-05-16 | Added ADR-015 Release Strategy to Key Spec Files table (Blue-Green deployment + release gates) | Human Dev | +| 1.9.3 | 2026-05-15 | ADR-023A: Model revision — gemma4:9b+Typhoon→gemma4:e4b Q8_0 (2-model stack), BullMQ 2-queue split, RAG full-doc embed, OCR auto-detect, n8n→DMS API boundary, QdrantService multi-tenancy contract | Windsurf AI | +| 1.9.2 | 2026-05-14 | Consolidated legacy AI ADRs (017, 017B, 018, 020, 022) into master ADR-023: Unified AI Architecture | Antigravity AI | +| 1.9.1 | 2026-05-13 | Added `bugfix` workflow and skill (migrated and improved from `docs/bugfix.md`) | Windsurf AI | +| 1.9.0 | 2026-05-03 | Integrated Global TypeScript Coding Standards (Headers, JSDoc, Thai comments, Single Export, No blank lines) | Windsurf AI | +| 1.8.5 | 2026-04-22 | Legacy version | Human Dev | diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..c923a0fa --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,147 @@ +# LCBP3 / NAP-DMS Context + +ระบบจัดการเอกสารงานก่อสร้าง (DMS) สำหรับโครงการ LCBP3 — เน้นการควบคุม Correspondence, RFA, Transmittal, Drawing พร้อมผู้ช่วย AI แบบ on-premises ที่ทำงานภายใต้ Workflow Engine กลางและขอบเขต AI ที่เข้มงวด (ADR-023A) + +## Language + +### Documents + +**Correspondence**: +ซองจดหมาย/เอกสารทุกประเภทที่หมุนเวียนในโครงการ เป็น parent ของ RFA / Transmittal / Memo +_Avoid_: Letter, Communication, Document (generic) + +**RFA** (Request For Approval): +Correspondence ประเภทขออนุมัติ มี revision และอ้างอิง Drawing Revision ผ่าน `rfa_items` +_Avoid_: Approval Request, Submit for Approval + +**Transmittal**: +Correspondence ที่ใช้ส่งมอบเอกสาร/แบบ ไม่ใช่จดหมายปะหน้า +_Avoid_: Delivery Note, Cover Letter + +**Shop Drawing**: +แบบที่ผู้รับเหมาจัดทำเพื่อขออนุมัติก่อนก่อสร้าง +_Avoid_: Construction Drawing + +**Contract Drawing**: +แบบต้นฉบับตามสัญญา ไม่ใช่ Shop Drawing +_Avoid_: Design Drawing, Blueprint + +### Workflow + +**Workflow Engine**: +State machine กลาง DSL-based (ADR-001) — authority เดียวของการเปลี่ยน state ของทุก Correspondence +_Avoid_: Approval Flow, Process Engine, RFA status flow (เป็นเพียง definition หนึ่ง) + +**Workflow Definition**: +Row ใน `workflow_definitions` ระบุ DSL ของ flow เช่น `RFA_FLOW_V1`, `CORRESPONDENCE_FLOW_V1` +_Avoid_: Approval logic, Hardcoded flow + +**Workflow Instance**: +Row ใน `workflow_instances` = สถานะปัจจุบันของเอกสารหนึ่งฉบับ — source of truth ของ state +_Avoid_: Status, Stage (ใช้ภายใน DSL ได้แต่ห้ามแทน instance) + +**Workflow Transition**: +การเปลี่ยน state ที่บันทึกใน `workflow_histories` พร้อม `actor_user_id` (มนุษย์เท่านั้น) +_Avoid_: Auto-execute, AI-driven approval + +### AI + +**AI Document Assistant**: +ผู้ช่วยที่ให้ Insight + Suggest + Notify โดยไม่เปลี่ยน state ของเอกสารเอง (ADR-023A) +_Avoid_: AI Document Controller, AI Agent, Autonomous Agent + +**AI Gateway**: +NestJS module ที่เป็นจุดเข้าเดียวของทุกคำขอ AI — enforce CASL + tenant scope ก่อนส่งงานเข้า BullMQ +_Avoid_: AI Service (generic), Tool Layer + +**Server-side Intent**: +Enum ของคำขอที่ AI Gateway รองรับ (เช่น `RAG_QUERY`, `CLASSIFY_DOCUMENT`, `EXTRACT_METADATA`) — แทนที่ LLM function-calling +_Avoid_: Tool, LLM tool, LangChain tool + +**Document Chunk**: +Row ใน `ai_document_chunks` (MariaDB) เก็บ chunk text + metadata, ground truth สำหรับ re-embed +_Avoid_: ai_embeddings, embedding row + +**Vector Point**: +Point ใน Qdrant — เก็บแค่ `chunk_public_id`, vector, และ payload `{ project_public_id, document_public_id, chunk_index }` +_Avoid_: Embedding (ambiguous), Vector record + +**RAG Query**: +Pipeline: embed query → `QdrantService.search(projectPublicId, vector)` → ดึง `chunk_text` จาก MariaDB → ส่งเข้า LLM พร้อม context +_Avoid_: Semantic search (overloaded), Vector search (incomplete) + +**OCR Service**: +Container สำเร็จรูป (opaque black box) เปิด HTTP API ให้ NestJS เรียก — ไม่มีโค้ด Python ใน repo, ทีมไม่ maintain runtime ภายใน +_Avoid_: Python sidecar, OCR microservice (ที่เรา maintain เอง) + +**Human-in-the-loop**: +ทุก AI suggestion ต้องผ่านการ accept/reject โดย user ก่อนกลายเป็น state change — บันทึกใน `ai_audit_logs` +_Avoid_: Auto-apply, AI auto-execute + +## Relationships + +- A **Correspondence** has a 1:1 specialization to **RFA** / **Transmittal** / etc. (table inheritance) +- A **RFA** has 1:N **RFA Revisions**, each linking to one or more **Shop Drawing Revisions** via `rfa_items` +- A **Workflow Instance** governs exactly one **Correspondence**; its current state is projected into entity columns (e.g. `rfa_revisions.rfa_status_code_id`) but **`workflow_instances` is the source of truth** +- A **Document Chunk** (MariaDB) has a 1:1 **Vector Point** in Qdrant via shared `chunk_public_id` (UUIDv7) +- An **AI Document Assistant** suggestion produces an `ai_audit_logs` row; if user accepts, it triggers a normal **Workflow Transition** (AI never writes the transition itself) +- **Qdrant queries MUST be filtered by `project_public_id`** — enforced at compile time by `QdrantService` signature + +## AI authority scope (resolved) + +| Scope | Allowed? | Mechanism | +|-------|----------|-----------| +| Read-only insight (summarise, explain) | ✅ | AI Gateway → service → CASL-guarded query | +| Suggest action (UI shows button) | ✅ | Response shape `{ suggestedAction, confidence, reasoning }` | +| Auto-trigger side-effects (notify, alert, comment) | ✅ | BullMQ job (ADR-008); MUST NOT change workflow state | +| Auto-execute workflow transition | ❌ | Forbidden Tier 1 — every transition needs human `actor_user_id` | + +## Upload pipeline (resolved) + +| Stage | Mode | Queue | Notes | +|-------|------|-------|-------| +| 1. Upload → **temp** + return `tempUploadId` | Sync | — | <1s | +| 2. ClamAV scan + MIME whitelist | Sync | — | block ก่อน commit (ADR-016) | +| 3. User commit (metadata + ย้าย permanent) | Sync | — | สร้าง `documents` row, ใช้ `Idempotency-Key` | +| 4. **Classification/Tagging** (3 pages แรก) | Async | `ai-realtime` | suggest metadata; user accept/reject (human-in-the-loop) | +| 5. **RAG Embedding** (full doc; OCR ถ้า text-layer < 100 chars/page) | Async | `ai-batch` | trigger AUTO หลัง commit, parallel กับ stage 4 | +| 6. Qdrant upsert + `ai_document_chunks.embedded_at = NOW()` | Async | (worker) | gap = DB full-text fallback | + +**กฎ:** +- ❌ ห้าม OCR/embed ใน HTTP request handler +- ✅ BullMQ `jobId = chunk_public_id` (UUIDv7) กัน duplicate +- ✅ Embed fail → graceful degrade (เอกสารยังใช้งานได้, AI feature ลด) +- ✅ Revision ใหม่ → chunks เก่า mark `superseded_at`, **ไม่ลบ** vector +- ✅ Frontend ใช้ `AiStatusBanner` แสดง progress + +## Example dialogue + +> **Dev:** "AI สรุป **RFA** revision นี้ให้หน่อย แล้วเปลี่ยน status เป็น approved เลย" +> **Domain expert:** "ไม่ได้ — AI สรุปได้ (read-only insight) และเสนอ 'ควร approve เพราะ…' ได้ (suggest action) แต่การเปลี่ยน state ต้องผ่าน user กดปุ่มเอง ระบบจะเรียก `WorkflowService.transition()` ซึ่งบันทึก `actor_user_id` เป็นมนุษย์ใน `workflow_histories`" + +> **Dev:** "งั้น **Tool Layer** ใน plan เก่าที่ให้ LLM เรียก `get_rfa(id)` ใช้ได้ไหม" +> **Domain expert:** "ไม่ใช่ tool ของ LLM — เป็น **Server-side Intent** ที่ AI Gateway แปลงเป็น service call ภายใต้ CASL + `projectPublicId` scope LLM แค่รับ context ที่ pre-fetched มาแล้ว" + +## Identifier rules (ADR-019, AI subsystem) + +| Boundary | Identifier ที่ใช้ | +|----------|-------------------| +| API (FE ↔ AI Gateway) | `publicId` (UUIDv7 string) เท่านั้น; INT `id` มี `@Exclude()` | +| Server-side Intent payload | `*PublicId` strings; service แปลงเป็น INT FK ภายใน | +| LLM context (prompt) | `publicId` + business code (`rfa_number`, `drawing_code`) ห้ามเห็น INT | +| Qdrant payload | `project_public_id`, `document_public_id`, `chunk_public_id` | +| `ai_document_chunks` internals | INT FK ใช้ได้ภายใน DB; identity ที่ expose = `chunk_public_id BINARY(16)` | +| Business codes (e.g. `drawing_code = "A-101"`) | รับเป็น input ได้ แต่ resolve → `publicId` ก่อน query | + +**Forbidden (Tier 1 CI blocker):** +- `parseInt(<*PublicId>)`, `Number(<*PublicId>)`, `+<*PublicId>` +- `publicId ?? id ?? ''` fallback chain +- DTO ที่มีทั้ง `{ id, uuid, publicId }` + +## Flagged ambiguities + +- **"approval logic"** ในเอกสารเก่าใช้คาบเกี่ยวระหว่าง `rfa_approve_codes` (business outcome เช่น 1A/1B) กับ `workflow_definitions` (state transition rules) — resolved: เป็นคนละสิ่ง +- **"ai_embeddings"** vs **"ai_document_chunks"** — resolved: ใช้ `ai_document_chunks` (metadata + text) + Qdrant (vector only); ห้ามเก็บ vector ใน MariaDB +- **"Tool Layer"** ในเอกสาร AI — resolved: ไม่ใช่ LLM-callable tools, เป็น **Server-side Intents** ที่ NestJS controlใน AI Gateway +- **"AI = Document Controller"** — resolved: ใช้ **AI Document Assistant** (Suggest + Insight) แทน เพื่อกัน scope creep ไปทาง autonomous agent +- **OpenRAG vs ADR-023A** — `specs/03-Data-and-Storage/03-07-OpenRAG.md` ระบุ Elasticsearch + dense_vector ซึ่งขัดกับ ADR-023A (Qdrant + nomic-embed-text) — **ยังไม่ resolve**, ต้องตัดสินใจในรอบถัดไป diff --git a/docs/AI Specs/Quizzes.md b/docs/AI Specs/Quizzes.md new file mode 100644 index 00000000..73e9b0c8 --- /dev/null +++ b/docs/AI Specs/Quizzes.md @@ -0,0 +1,421 @@ +🧠 🎯 เป้าหมาย: AI DMS (Document Management System อัจฉริยะ) + +> จาก “เก็บเอกสาร” → กลายเป็น “ผู้ช่วยควบคุมเอกสาร” + +--- + +🏗️ 1. Architecture ภาพรวม + +[User] +↓ +(Web UI / Chat UI) +↓ +AI Agent Layer ← (หัวใจ) +↓ + +| RAG Engine (ค้นหาเอกสาร) +| Workflow Engine (RFA logic) +| Tool Layer (DB / File / API) + +↓ +[Database + File Storage] + +--- + +🔥 2. Core Components (ต้องมี) + +2.1 AI Agent (สมอง) + +ใช้แนวคิดจาก: + +LangChain / OpenClaw / CrewAI + +💡 ทำอะไร: + +เข้าใจคำถาม user + +ตัดสินใจว่าจะ: + +query DB + +search document + +run workflow + + +--- + +2.2 RAG System (ค้นหาเอกสาร) + +💡 ใช้: + +vector DB (เช่น Qdrant / Chroma) + +💡 ทำอะไร: + +search PDF / Drawing / Spec + +ตอบคำถามจาก content + +👉 use case: + +“Drawing A-101 revision ล่าสุดคืออะไร” + +“RFA นี้เกี่ยวข้องกับ drawing ไหน” + + +--- + +2.3 Tool Layer (สำคัญมาก) + +AI จะไม่เก่งถ้าไม่มี tools + +tools ที่ต้องมี: + +get_rfa(id) + +get_drawings_by_rfa(rfa_id) + +get_latest_revision(drawing_code) + +search_documents(query) + +get_transmittal_history() + +👉 ตรงนี้ = bridge ระหว่าง AI กับ DB จริงของคุณ + + +--- + +2.4 Workflow Engine (logic ธุรกิจ) + +💡 ไม่ให้ AI มั่ว + +เช่น: + +RFA status flow + +approval logic + +revision control + +👉 AI = “ช่วยคิด” +👉 workflow = “ของจริง” + + +--- + +2.5 File Storage + +เก็บไฟล์จริง (PDF, DWG) + +version control + +naming convention + +👉 ตรงกับ backlog คุณข้อ 9 พอดี + + +--- + +⚙️ 3. Database Design (ต่อยอดของเดิมคุณ) + +คุณมีอยู่แล้ว: + +RFAs + +Drawings + +Revisions + +👉 เพิ่ม: + +🧩 ai_embeddings + +document_id + +chunk_text + +vector + +🧩 ai_logs + +user_query + +agent_decision + +tool_used + +🧩 ai_tasks (optional) + +background job + +document indexing + + +--- + +🤖 4. AI Use Cases (ของจริงที่ควรทำ) + +🔎 1. Smart Search + +> “ขอ drawing structural ล่าสุดของ zone B” + + + +AI: + +เข้าใจ intent + +query DB + RAG + + +--- + +📄 2. Document QA + +> “สรุป spec นี้” + + + +AI: + +อ่าน PDF + +summarize + + +--- + +🔗 3. Relationship Mapping + +> “RFA นี้เกี่ยวกับ drawing อะไร” + + + +AI: + +join: + +rfa → drawing → revision + + +--- + +📊 4. Timeline Analysis + +> “RFA นี้ delay เพราะอะไร” + + + +AI: + +วิเคราะห์ revision timeline + + +--- + +🧠 5. Auto Classification + +upload file → AI tag: + +type + +discipline + +revision + + +--- + +⚠️ 6. Alert / Assistant + +“Drawing นี้ outdated” + +“RFA ใกล้ deadline” + + +--- + +🧩 5. UI Design (สำคัญมาก) + +5.1 Hybrid UI + +Table (DataTables) + +Chat (AI assistant) + +[ Table RFAs ] | [ AI Chat ] +| > RFA ล่าสุดคืออะไร +| > Drawing ไหนยังไม่ approve + +👉 best of both worlds + + +--- + +5.2 Drawing Page (โคตรสำคัญ) + +แสดง: + +revision history + +related RFAs + +AI summary + + +--- + +⚡ 6. Tech Stack (แนะนำตรงๆ) + +Backend + +PHP (ของเดิมคุณ) + +Python (AI service) + +AI Layer + +LangChain หรือ custom agent + +Ollama (local) หรือ OpenAI + +Vector DB + +Qdrant (แนะนำ) + +Frontend + +DataTables (ตาม backlog) + +Chat UI + + +--- + +🔥 7. Flow การทำงานจริง + +📥 Upload Drawing + +1. user upload PDF + + +2. system: + + + +extract text (OCR) + +chunk + +embed → vector DB + +3. AI tag metadata + + + + +--- + +🔎 Query + +user: + +> “drawing ล่าสุดของ contract นี้” + + + +AI: + +1. เข้าใจ intent + + +2. call tool: + + + +get_drawings(contract_id) + +3. filter revision ล่าสุด + + +4. ตอบ + + + + +--- + +🧠 8. Insight สำคัญ (อย่าพลาด) + +❌ อย่าทำแบบนี้ + +ให้ AI query DB ตรงๆ + +ให้ AI ตัดสินใจ workflow + +✅ ต้องทำแบบนี้ + +AI → call function (tool) + +system → validate logic + + +--- + +🚀 9. Roadmap (ทำจริง) + +Phase 1 (2–4 สัปดาห์) + +AI chat + query DB + +basic tools + +Phase 2 + +RAG (search document) + +embedding + +Phase 3 + +automation + +alert + +classification + + +--- + +🎯 10. ถ้าจะทำให้ “โหดจริง” + +👉 ultimate version: + +AI = Document Controller + +auto check: + +revision mismatch + +missing approval + +outdated drawing + + +--- + +💡 สรุปสั้น + +AI DMS = + +> “AI + Workflow + Document + Database” + + +ไม่ใช่แค่ chatbot + + +--- + +จากเนื้อหาข้างบน เทียบกับ repo ของผม ขอสรุป แบบละเฮียกทีละข้อ diff --git a/docs/AI Specs/deepseek-answer.md b/docs/AI Specs/deepseek-answer.md new file mode 100644 index 00000000..08974c3e --- /dev/null +++ b/docs/AI Specs/deepseek-answer.md @@ -0,0 +1,189 @@ +จากการวิเคราะห์ repo `lcbp3` อย่างละเอียด เทียบกับ blueprint AI DMS ของคุณแบบข้อต่อข้อ: + +--- + +## 🔍 สรุปเทียบ Blueprint AI DMS กับ Repo `lcbp3` + +--- + +### 🏗️ 1. Architecture ภาพรวม + +| Blueprint | Repo `lcbp3` | สถานะ | +|---|---|---| +| Web UI / Chat UI | ✅ Next.js 16.2.0 + shadcn/ui + `RagChatWidget.tsx` | **มีแล้ว** | +| AI Agent Layer | ✅ `AiService` + `AiRagService` + `AiQueueService` — ใช้ **n8n + Ollama** pipeline ตาม ADR-023 | **มีแล้ว** (คนละ approach) | +| RAG Engine | ✅ `AiRagService` + `AiQdrantService` (Qdrant vector DB, project-isolated) | **มีแล้ว** | +| Workflow Engine | ✅ ADR-021 Unified Workflow Engine (DSL-based สำหรับ Correspondences, RFAs, Circulations) | **มีแล้ว** | +| Tool Layer | ⚠️ มี AI services (embedding, OCR, Ollama, Qdrant, migration) แต่ไม่มี tool functions ที่ blueprint ระบุ (เช่น `get_rfa(id)`, `get_drawings_by_rfa(rfa_id)`) ใน AI module | **บางส่วน** | +| DB + File Storage | ✅ MariaDB 11.8 + Two-Phase File Storage (Multer + ClamAV) | **มีแล้ว** | + +**💡 ความเห็น:** โครงสร้างพื้นฐานตรงตาม blueprint ~85% แต่ AI Agent Layer ใช้ n8n+Ollama แทน LangChain/CrewAI ที่ blueprint แนะนำ ซึ่งเป็น architectural decision ที่ **ถูกต้องแล้ว** สำหรับบริบทของคุณ (local deployment บน QNAP, เน้น privacy, no external API) — ADR-023 รวม ADR-017, 017B, 018, 020, 022 เข้าด้วยกันเป็น Unified AI Architecture + +--- + +### 🔥 2. Core Components (ต้องมี) + +#### 2.1 AI Agent (สมอง) +| Blueprint แนะนำ | Repo จริง | หมายเหตุ | +|---|---|---| +| LangChain / OpenClaw / CrewAI | **n8n + Ollama** | ADR-023 เลือก n8n workflow orchestration + Ollama local LLM inference — pragmatic choice สำหรับ QNAP NAS ที่มี RAM 32GB | + +**💡 ความเห็น:** n8n เป็น low-code workflow automation ที่ stable กว่า LangChain สำหรับ production use case นี้ และ Ollama รองรับการรัน local LLM บน CPU ได้ดี จุดที่ยังขาดคือ **agentic decision-making** — ตอนนี้ AI ยังไม่สามารถ "ตัดสินใจว่าจะ query DB / search document / run workflow" ได้เอง ต้องต่อยอดจาก n8n workflow + AI services ที่มีอยู่ + +#### 2.2 RAG System (ค้นหาเอกสาร) +| Blueprint | Repo | สถานะ | +|---|---|---| +| Vector DB (Qdrant/Chroma) | ✅ **Qdrant** — `AiQdrantService` ใช้ `@qdrant/js-client-rest`, collection `lcbp3_vectors`, 768-dim vectors, Cosine distance, **project-isolated** payload filter | **ครบ** | +| Search PDF/Drawing/Spec | ✅ `AiRagService` — query ผ่าน Ollama + Qdrant, BullMQ-backed async pipeline, Redis caching, TTL 5 นาที, FR-009 (1 active job per user) | **ครบ** | +| Embedding | ✅ `embedding.service.ts` + Ollama embed model (`nomic-embed-text`) | **มีแล้ว** | +| Chunking | ⚠️ มี `ai-ingest.service.ts` สำหรับ document ingestion แต่ไม่เห็น chunking strategy ชัดเจน | **ต้องตรวจสอบ** | +| Use case: "Drawing A-101 revision ล่าสุด" | ❌ ยังไม่เห็น integration ระหว่าง RAG search ↔ drawing module โดยตรง | **ยังไม่มี** | + +#### 2.3 Tool Layer +| Blueprint Tool | Repo | สถานะ | +|---|---|---| +| `get_rfa(id)` | ❌ ไม่มีใน AI module (มีใน RFA module แต่ AI เรียกตรงไม่ได้) | **ต้องพัฒนา** | +| `get_drawings_by_rfa(rfa_id)` | ❌ ไม่มี cross-module tool | **ต้องพัฒนา** | +| `get_latest_revision(drawing_code)` | ❌ ไม่มีใน AI module | **ต้องพัฒนา** | +| `search_documents(query)` | ✅ มีผ่าน RAG + Elasticsearch | **มีแล้ว** | +| `get_transmittal_history()` | ❌ ไม่มี | **ต้องพัฒนา** | + +**💡 Insight:** ตรงนี้คือ **gap สำคัญที่สุด** — blueprint บอกว่า tool layer คือ "bridge ระหว่าง AI กับ DB จริง" แต่ repo ตอนนี้ AI module แยกขาดจาก business modules (RFA, Drawing, Transmittal) ตาม ADR-018 AI Boundary Policy (ต่อมาถูกรวมเข้า ADR-023) ที่กำหนดว่า **"Ollama Isolation มี No Direct DB/Storage Access"** — นี่คือ security boundary ที่ดี แต่ต้องสร้าง tool functions เป็น bridge จริงๆ + +#### 2.4 Workflow Engine +| Blueprint | Repo | สถานะ | +|---|---|---| +| RFA status flow | ✅ `workflow-engine` module + `rfas` components | **มีแล้ว** | +| Approval logic | ✅ ADR-021 Integrated Context Workflow | **มีแล้ว** | +| Revision control | ✅ Drawing module มี revision tracking | **มีแล้ว** | + +#### 2.5 File Storage +| Blueprint | Repo | สถานะ | +|---|---|---| +| เก็บไฟล์จริง (PDF, DWG) | ✅ Two-Phase File Storage, Multer + ClamAV virus scanning | **มีแล้ว** | +| Version control | ✅ Schema v1.8.0 รองรับ | **มีแล้ว** | +| Naming convention | ✅ Document Numbering (ADR-002, Double-lock mechanism) | **มีแล้ว** | + +--- + +### ⚙️ 3. Database Design + +| Blueprint Suggestion | Repo | สถานะ | +|---|---|---| +| `ai_embeddings` (document_id, chunk_text, vector) | ✅ Qdrant เก็บ vectors แยกจาก MariaDB, collection `lcbp3_vectors` พร้อม payload (project_public_id, public_id) | **มีแล้ว** (คนละ implementation) | +| `ai_logs` (user_query, agent_decision, tool_used) | ✅ `ai-audit-log.entity.ts` + `AuditLog` entity — บันทึก AI jobs, callbacks, results | **มีแล้ว** | +| `ai_tasks` (background job, indexing) | ✅ BullMQ queues (`ai-realtime`, `ai-batch`) + `MigrationLog` entity | **มีแล้ว** | +| RFAs, Drawings, Revisions | ✅ มีครบทุก module | **มีแล้ว** | + +--- + +### 🤖 4. AI Use Cases (ของจริง) + +| Use Case | สถานะใน Repo | +|---|---| +| **1. Smart Search** — "ขอ drawing structural ล่าสุดของ zone B" | ⚠️ RAG search มีแล้ว + Elasticsearch แต่ยังไม่รวม intent understanding + cross-module query | +| **2. Document QA** — "สรุป spec นี้" | ⚠️ `AiRagService` รองรับ Q&A บน document content ผ่าน Ollama — มีพื้นฐานแล้ว | +| **3. Relationship Mapping** — "RFA นี้เกี่ยวกับ drawing อะไร" | ❌ ยังไม่มี cross-module relationship query | +| **4. Timeline Analysis** — "RFA นี้ delay เพราะอะไร" | ❌ ยังไม่มี | +| **5. Auto Classification** — upload file → AI tag | ✅ `ai-ingest.service.ts` + metadata extraction + AI Suggestion (`POST /ai/suggest`) | +| **6. Alert / Assistant** — "Drawing นี้ outdated", "RFA ใกล้ deadline" | ⚠️ มี `reminder` module + BullMQ queues แต่ยังไม่ integrated กับ AI | + +--- + +### 🧩 5. UI Design + +| Blueprint | Repo | สถานะ | +|---|---|---| +| Hybrid UI (Table + Chat) | ✅ DataTables-style + `RagChatWidget.tsx` + `rag-search-bar.tsx` + `rag-result-card.tsx` | **มีแล้ว** | +| Drawing Page (revision history, related RFAs, AI summary) | ✅ Drawing module + revision tracking + AI components (`document-comparison-view.tsx`) | **มีแล้ว** | +| AI Status Banner | ✅ `AiStatusBanner.tsx` | **มีแล้ว** | + +--- + +### ⚡ 6. Tech Stack + +| Blueprint แนะนำ | Repo จริง | หมายเหตุ | +|---|---|---| +| **PHP** (ของเดิมคุณ) | **NestJS 11 (TypeScript)** | คุณเปลี่ยน stack ไปแล้ว — repo เป็น TypeScript 87.5% | +| **Python** (AI service) | **TypeScript + n8n + Ollama** | ไม่มี Python service แยก — AI logic อยู่ใน NestJS module + n8n workflows | +| **LangChain** | **n8n + custom services** | Architectural decision ที่ pragmatic — n8n เสถียรกว่า LangChain สำหรับ production | +| **Qdrant** | ✅ `@qdrant/js-client-rest` | ตรงตาม blueprint | +| **Ollama** | ✅ Ollama บน QNAP NAS | ตรงตาม blueprint | +| **DataTables + Chat UI** | ✅ shadcn/ui + RagChatWidget | ตรงตาม blueprint | + +--- + +### 🔥 7. Flow การทำงานจริง + +**📥 Upload Drawing Flow:** +``` +User upload → Multer + ClamAV scan → Two-Phase File Storage → +ai-ingest.service (extract text, OCR, embed) → Qdrant upsert → AI tag metadata +``` +✅ มีครบตาม blueprint + +**🔎 Query Flow:** +``` +User: "drawing ล่าสุดของ contract นี้" → +RagChatWidget → AiRagService → Qdrant search → Ollama generate answer → +แสดงผลพร้อม citations +``` +⚠️ มี RAG search แต่ยังขาด tool call ไปยัง drawing/contract modules + +--- + +### 🧠 8. Insight สำคัญ — สิ่งที่ทำถูกแล้วและที่ต้องระวัง + +| หลักการ | Repo | คะแนน | +|---|---|---| +| ❌ อย่าให้ AI query DB ตรงๆ | ✅ ADR-023: Ollama Isolation, No Direct DB Access | **ดีมาก** | +| ❌ อย่าให้ AI ตัดสินใจ workflow | ✅ Workflow Engine (ADR-021) แยกจาก AI | **ดีมาก** | +| ✅ AI → call function (tool) | ⚠️ ยังไม่มี tool function layer เชื่อม AI ↔ business modules | **ต้องพัฒนา** | +| ✅ System → validate logic | ✅ Validation services, RBAC guards, Business Logic Guards (37 edge cases) | **ดีมาก** | + +--- + +### 🚀 9. Roadmap — สถานะเทียบ Blueprint + +| Blueprint Phase | Repo Status | +|---|---| +| **Phase 1:** AI chat + query DB + basic tools | ✅ v1.9.0 — `RagChatWidget` + RAG search + AI Suggestion endpoints | +| **Phase 2:** RAG + embedding | ✅ v1.9.2 — Qdrant collection, embedding service, RAG pipeline | +| **Phase 3:** Automation, alert, classification | 🔄 กำลังพัฒนา — AI Suggest, reminder module, n8n workflows | + +--- + +### 🎯 10. Ultimate Version — "AI = Document Controller" + +| ความสามารถ | สถานะ | +|---|---| +| Auto check revision mismatch | ❌ ยังไม่มี | +| Missing approval detection | ❌ ยังไม่มี | +| Outdated drawing alert | ❌ ยังไม่มี | + +--- + +## 💡 สรุปภาพรวม + +**repo `lcbp3` v1.9.2 ของคุณทำไปแล้ว ~70-80% ของ blueprint AI DMS** + +### ✅ สิ่งที่มีแล้ว (แข็งแรงมาก): +- **RAG System** — Qdrant + Ollama + BullMQ pipeline, project-isolated, production-ready +- **AI Chat UI** — `RagChatWidget` + search bar + result cards +- **Workflow Engine** — DSL-based, รองรับ RFA, Correspondence, Circulation +- **File Storage** — Two-phase + virus scanning + version control +- **AI Boundary** — Security isolation ที่ดี (Ollama ไม่แตะ DB โดยตรง) +- **Infrastructure** — Docker Compose hardened, Blue-Green Deploy, 0 vulnerabilities +- **Documentation** — 10/10 Gaps Closed, 23 ADRs, specs ครอบคลุมทุกมิติ + +### ❌ สิ่งที่ยังขาด (ตาม blueprint): +1. **Tool Layer** — ยังไม่มี tool functions เชื่อม AI กับ business modules (`get_rfa`, `get_drawings_by_rfa`, `get_latest_revision` ฯลฯ) — **นี่คือ gap ใหญ่สุด** +2. **Agentic Decision-Making** — AI ยังตอบได้เฉพาะ RAG search ยังตัดสินใจเองไม่ได้ว่าจะเรียก tool ไหน +3. **Cross-Module Relationship Mapping** — ยังไม่มี RFA ↔ Drawing ↔ Revision linkage ใน AI context +4. **Auto Document Controller** — ยังไม่ถึงขั้น AI ตรวจสอบ revision mismatch, missing approval อัตโนมัติ + +### 🎯 สิ่งที่ควรทำต่อ (Priority): +1. **สร้าง Tool Functions** ใน AI module ที่เป็น bridge ไปยัง RFA, Drawing, Transmittal modules — โดยคง security boundary ไว้ +2. **เพิ่ม Intent Routing** — ให้ AI รู้ว่าคำถามแบบไหนควรเรียก tool ไหน (ต่อยอดจาก n8n workflow ที่มี) +3. **Cross-Module Context** — inject RFA-Drawing-Revision relationships เข้า RAG context +4. **Alert Engine** — ต่อยอด reminder module + AI classification เพื่อแจ้งเตือนอัจฉริยะ diff --git a/docs/AI Specs/qwen-answer.md b/docs/AI Specs/qwen-answer.md new file mode 100644 index 00000000..a61068b7 --- /dev/null +++ b/docs/AI Specs/qwen-answer.md @@ -0,0 +1,294 @@ +# 🔍 เปรียบเทียบ: AI DMS Architecture Proposal vs. Repo จริง (lcbp3) + +> จากแผน "อยากได้" → เทียบกับ "ที่มีอยู่จริง" แบบละเฮียกทีละข้อ 🎯 + +--- + +## 🏗️ 1. Architecture ภาพรวม + +| ส่วนที่เสนอ | สถานะใน Repo | Gap / ข้อสังเกต | +|------------|-------------|---------------| +| **User → Web UI / Chat UI** | ✅ มี Next.js 16 + App Router | ✅ ตรงตามแผน แต่ยังไม่มี "Chat UI" แบบ Dedicated (มีแต่ Table + Form) | +| **AI Agent Layer** | 🟡 มี `.agents/` + `AGENTS.md` + n8n | ⚠️ เป็น "Agent Toolkit" สำหรับ Dev AI ช่วยเขียนโค้ด **ไม่ใช่** AI Agent ที่รันใน Production สำหรับ User | +| **RAG Engine** | ✅ มี ADR-022 (Superseded) → ADR-023 | ✅ มีสถาปัตยกรรม RAG แล้ว แต่ยังเป็น "แผน" ในเอกสาร ต้องดูการ Implement จริง | +| **Workflow Engine** | ✅ มี `workflow-engine/` module + ADR-001/021 | ✅ ตรงตามแผน 100% — DSL-based workflow + Integrated Context | +| **Tool Layer** | 🟡 มี Services ใน NestJS modules | ⚠️ Tools ยังเป็น "Internal API" ไม่ใช่ "Function Calling Interface" สำหรับ AI โดยตรง | +| **Database + Storage** | ✅ MariaDB 11.8 + Two-Phase File Storage | ✅ ตรงตามแผน + มี ClamAV + Redis Locking | + +👉 **สรุป**: โครงสร้างพื้นฐาน (Infrastructure) พร้อมมาก แต่ "AI Agent Layer" ที่ User คุยด้วยยังต้องพัฒนาเพิ่ม + +--- + +## 🔥 2. Core Components (ต้องมี) + +### 2.1 AI Agent (สมอง) + +| ที่เสนอ | ที่มีอยู่ | Gap | +|--------|----------|-----| +| LangChain / CrewAI | 🟡 ใช้ n8n + Ollama (ADR-023) | ⚠️ n8n เป็น Workflow Orchestrator **ไม่ใช่** Agent Framework แบบ LangChain | +| เข้าใจคำถาม → ตัดสินใจ → Call Tool | 🟡 มี "AI Boundary" (ADR-023) | ⚠️ ยังไม่มี "Intent Recognition Layer" ที่แยก Query → Tool Routing ชัดเจน | + +💡 **แนะนำ**: ถ้าอยากได้ Agent แบบ "ตัดสินใจเอง" อาจต้องเพิ่ม `ai-agent/` module ที่ใช้ LangGraph หรือ Custom Agent Loop + +--- + +### 2.2 RAG System (ค้นหาเอกสาร) + +| ที่เสนอ | ที่มีอยู่ | Gap | +|--------|----------|-----| +| Vector DB: Qdrant / Chroma | 🟡 มี ADR-023A ระบุใช้ `Qdrant` | ✅ มีในสถาปัตยกรรม แต่ต้องตรวจสอบว่า Deploy แล้วหรือยัง | +| Search PDF / Drawing / Spec | 🟡 มี Elasticsearch 9.3.4 | ⚠️ Elasticsearch เป็น **Keyword Search** ไม่ใช่ **Vector Search** — ต้องแยกกัน | +| Use case: "Drawing A-101 revision ล่าสุด" | 🟡 มี `drawing/` module + revision tracking | ✅ Data Model พร้อม แต่ต้องเพิ่ม "Natural Language → Query Translation" | + +💡 **จุดสำคัญ**: ต้องแยกให้ชัดระหว่าง: +- `Elasticsearch` → Full-text search (keyword) +- `Qdrant` → Vector search (semantic/RAG) + +--- + +### 2.3 Tool Layer (สำคัญมาก) ⭐ + +| Tool ที่เสนอ | ที่มีอยู่ (NestJS Service) | Gap | +|-------------|--------------------------|-----| +| `get_rfa(id)` | ✅ `RfaService.findOne()` | ✅ พร้อม แต่ต้องสร้าง "AI Tool Wrapper" | +| `get_drawings_by_rfa(rfa_id)` | 🟡 มี Relation ใน Schema | ⚠️ ต้องเขียน Service Method แยกสำหรับ AI Call | +| `get_latest_revision(drawing_code)` | ✅ มี `DrawingService.getLatestRevision()` | ✅ พร้อม | +| `search_documents(query)` | ✅ มี `SearchModule` (Elasticsearch) | ⚠️ ต้องเพิ่ม "Vector Search" endpoint สำหรับ RAG | +| `get_transmittal_history()` | ✅ มี `TransmittalService` | ✅ พร้อม | + +💡 **สิ่งที่ต้องทำเพิ่ม**: สร้าง `AiToolsService` ที่: +```typescript +// ตัวอย่าง: AI Tool Wrapper +@Injectable() +export class AiToolsService { + @Tool({ name: 'get_rfa', description: 'Get RFA by ID' }) + async getRfa(id: string) { + return this.rfaService.findOne({ publicId: id }); + } + // ...อื่นๆ +} +``` + +--- + +### 2.4 Workflow Engine (logic ธุรกิจ) + +| ที่เสนอ | ที่มีอยู่ | Gap | +|--------|----------|-----| +| RFA status flow | ✅ มี `workflow-engine/` + DSL | ✅ ตรงตามแผน 100% | +| Approval logic | ✅ มี CASL Guards + State Machine | ✅ พร้อม | +| Revision control | ✅ มี `revisions` table + locking | ✅ พร้อม | +| "AI = ช่วยคิด, Workflow = ของจริง" | ✅ มี ADR-023 "AI Boundary" | ✅ ตรงตามหลักการ | + +👉 **จุดแข็ง**: ส่วนนี้ทำไว้ดีมากแล้ว — AI ไม่สามารถ "มั่ว" Workflow ได้ เพราะต้องผ่าน DSL Engine + +--- + +### 2.5 File Storage + +| ที่เสนอ | ที่มีอยู่ | Gap | +|--------|----------|-----| +| เก็บไฟล์จริง (PDF, DWG) | ✅ Two-Phase Storage + ClamAV | ✅ ตรงตามแผน | +| Version control | ✅ มี `revisions` table + naming convention | ✅ พร้อม | +| Naming convention | ✅ มี `document-numbering/` module (ADR-002) | ✅ พร้อม | + +--- + +## ⚙️ 3. Database Design + +| Table ที่เสนอ | ที่มีอยู่ | Gap | +|--------------|----------|-----| +| `ai_embeddings` | 🟡 มีใน ADR-023A (Qdrant) | ⚠️ ต้องสร้าง Collection ใน Qdrant + Sync Job | +| `ai_logs` | 🟡 มี `monitoring/` module | ⚠️ ต้องเพิ่ม `ai_audit_logs` table สำหรับ tracking AI decisions | +| `ai_tasks` | 🟡 มี BullMQ (ADR-008) | ✅ สามารถใช้ `ai-batch` queue สำหรับ background jobs | + +💡 **แนะนำ**: สร้าง migration script สำหรับ: +```sql +-- ตัวอย่าง: ai_audit_logs table +CREATE TABLE ai_audit_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_public_id BINARY(16), + query_text TEXT, + agent_decision JSON, + tools_used JSON, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 🤖 4. AI Use Cases (ของจริงที่ควรทำ) + +| Use Case | สถานะใน Repo | ความยาก | +|----------|-------------|---------| +| 🔎 Smart Search | 🟡 มี Elasticsearch + ADR-023 RAG | ⭐⭐ ต้องเพิ่ม NL→Query translator | +| 📄 Document QA | 🟡 มี OCR + Text extraction ในแผน | ⭐⭐⭐ ต้องเพิ่ม PDF parsing + chunking pipeline | +| 🔗 Relationship Mapping | ✅ มี Relations ใน Schema + Services | ⭐⭐ ต้องเพิ่ม "Graph Query" layer สำหรับ AI | +| 📊 Timeline Analysis | 🟡 มี `revisions.created_at` | ⭐⭐⭐ ต้องเพิ่ม "Temporal Query" logic | +| 🧠 Auto Classification | 🟡 มี ADR-017B (Superseded) | ⭐⭐⭐⭐ ต้องฝึก Model หรือใช้ Prompt Engineering | +| ⚠️ Alert / Assistant | 🟡 มี `notification/` module | ⭐⭐ ต้องเพิ่ม "Rule Engine" สำหรับ trigger alerts | + +--- + +## 🧩 5. UI Design + +| ส่วนที่เสนอ | ที่มีอยู่ | Gap | +|------------|----------|-----| +| Hybrid UI: Table + Chat | 🟡 มี DataTables + Forms | ⚠️ ยังไม่มี "Chat Panel" แบบ Side-by-side | +| Drawing Page: revision + RFAs + AI summary | ✅ มี `drawing/` module + relations | ⚠️ ต้องเพิ่ม "AI Summary" component ที่เรียก RAG | + +💡 **Quick Win**: เพิ่ม Chat Panel แบบง่ายๆ ใน `app/(dashboard)/drawings/[id]/page.tsx`: +```tsx +// ตัวอย่าง: Chat Panel ใน Drawing Page + +``` + +--- + +## ⚡ 6. Tech Stack + +| ส่วนที่เสนอ | ที่มีอยู่ | ตรงกัน? | +|------------|----------|---------| +| Backend: PHP → Python | ❌ ใช้ NestJS (TypeScript) ทั้งระบบ | ⚠️ ไม่ตรง — แต่ TypeScript + NestJS ดีกว่า PHP สำหรับ AI Integration | +| AI Layer: LangChain / Ollama | 🟡 ใช้ Ollama + n8n (ADR-023) | ✅ ตรงบางส่วน — ขาด LangChain/LlamaIndex | +| Vector DB: Qdrant | ✅ มีใน ADR-023A | ✅ ตรง | +| Frontend: DataTables + Chat UI | 🟡 มี DataTables (shadcn/ui) | ⚠️ ขาด Chat UI component | + +💡 **ข้อดีของ Stack ปัจจุบัน**: +- Full TypeScript → Type-safe AI Tool definitions +- NestJS Modules → Easy to add `ai-agent/` module +- BullMQ → Ready for async AI jobs + +--- + +## 🔥 7. Flow การทำงานจริง + +### 📥 Upload Drawing + +| ขั้นตอนที่เสนอ | ที่มีอยู่ | Gap | +|--------------|----------|-----| +| 1. User upload PDF | ✅ มี Two-Phase Upload | ✅ พร้อม | +| 2. Extract text (OCR) | 🟡 มีแผนใน ADR-023A | ⚠️ ต้องเพิ่ม OCR service (Tesseract / PaddleOCR) | +| 3. Chunk + Embed → Vector DB | 🟡 มี Qdrant ในแผน | ⚠️ ต้องเขียน `DocumentIndexingService` | +| 4. AI tag metadata | 🟡 มี ADR-017B (Superseded) | ⚠️ ต้อง implement classification pipeline | + +### 🔎 Query + +| ขั้นตอนที่เสนอ | ที่มีอยู่ | Gap | +|--------------|----------|-----| +| 1. เข้าใจ intent | 🟡 ไม่มี NLU layer | ⭐⭐⭐ ต้องเพิ่ม Intent Classifier | +| 2. Call tool: `get_drawings()` | ✅ มี `DrawingService` | ✅ พร้อม (แต่ต้อง wrap เป็น AI Tool) | +| 3. Filter revision ล่าสุด | ✅ มี `getLatestRevision()` | ✅ พร้อม | +| 4. ตอบ | 🟡 มี API Response | ⚠️ ต้องเพิ่ม "Natural Language Generation" layer | + +--- + +## 🧠 8. Insight สำคัญ (อย่าพลาด) + +| ข้อควรระวัง | สถานะใน Repo | ผ่าน? | +|------------|-------------|-------| +| ❌ อย่าให้ AI query DB ตรงๆ | ✅ มี ADR-023 "AI Boundary" | ✅ ผ่าน | +| ❌ อย่าให้ AI ตัดสินใจ workflow | ✅ มี Workflow Engine แยก | ✅ ผ่าน | +| ✅ AI → call function (tool) | 🟡 มี Services แต่ไม่มี Tool Interface | ⚠️ ต้องเพิ่ม `@Tool()` decorator pattern | +| ✅ System → validate logic | ✅ มี CASL Guards + Validation | ✅ ผ่าน | + +👉 **คุณทำถูกแล้ว**: การแยก "AI Intelligence" ออกจาก "Business Logic" เป็นหัวใจของระบบที่ปลอดภัย + +--- + +## 🚀 9. Roadmap (ทำจริง) + +### Phase 1 (2–4 สัปดาห์) — AI Chat + Query DB + +| Task | สถานะ | ความยาก | +|------|--------|---------| +| สร้าง `AiAgentModule` ใน NestJS | ❌ ยังไม่มี | ⭐⭐ | +| สร้าง `AiToolsService` wrapper | ❌ ยังไม่มี | ⭐⭐ | +| เพิ่ม Chat UI component ใน Frontend | ❌ ยังไม่มี | ⭐⭐ | +| ทดสอบ: "RFA ล่าสุดคืออะไร" | ❌ ยังไม่มี | ⭐⭐ | + +### Phase 2 — RAG + Embedding + +| Task | สถานะ | ความยาก | +|------|--------|---------| +| Deploy Qdrant ใน Docker | 🟡 มีในแผน | ⭐⭐ | +| สร้าง `DocumentIndexingService` (OCR + Chunk + Embed) | ❌ ยังไม่มี | ⭐⭐⭐ | +| สร้าง `QdrantService` สำหรับ vector search | ❌ ยังไม่มี | ⭐⭐ | +| ทดสอบ: "สรุป spec นี้" | ❌ ยังไม่มี | ⭐⭐⭐ | + +### Phase 3 — Automation + Alert + +| Task | สถานะ | ความยาก | +|------|--------|---------| +| สร้าง `AiAlertService` สำหรับ rule-based alerts | ❌ ยังไม่มี | ⭐⭐⭐ | +| สร้าง `ClassificationPipeline` สำหรับ auto-tagging | ❌ ยังไม่มี | ⭐⭐⭐⭐ | +| Integrateกับ Notification Module | ✅ มี `notification/` module | ⭐⭐ | + +--- + +## 🎯 10. ถ้าจะทำให้ "โหดจริง" + +### Ultimate Version: AI = Document Controller + +| ฟีเจอร์ | สิ่งที่ต้องเพิ่ม | ความยาก | +|--------|----------------|---------| +| 🔍 Auto check: revision mismatch | สร้าง `RevisionConsistencyChecker` | ⭐⭐⭐ | +| 🔍 Auto check: missing approval | สร้าง `ApprovalGapDetector` | ⭐⭐⭐ | +| 🔍 Auto check: outdated drawing | สร้าง `DrawingFreshnessMonitor` | ⭐⭐⭐ | +| 🤖 Proactive alert: "Drawing นี้ outdated" | Integrateกับ `notification/` module | ⭐⭐ | + +--- + +## 💡 สรุปสั้นๆ (แบบเฮียก) + +### ✅ สิ่งที่ "มีแล้ว" และ "ทำดีแล้ว" +1. **สถาปัตยกรรมพื้นฐาน** — NestJS + Next.js + MariaDB + Redis + Elasticsearch → Solid +2. **Workflow Engine** — DSL-based + ADR-001/021 → ตรงตามแผน 100% +3. **Security & RBAC** — CASL + 4-Level + JWT → พร้อมสำหรับ Enterprise +4. **File Storage** — Two-Phase + ClamAV + Versioning → ดีมาก +5. **AI Boundary** — ADR-023 กำหนดขอบเขตชัดเจน → ป้องกัน AI มั่ว + +### ⚠️ สิ่งที่ "ต้องเพิ่ม" เพื่อให้เป็น AI DMS จริง +1. **AI Agent Runtime** — สร้าง `ai-agent/` module ที่รันใน Production (ไม่ใช่แค่ `.agents/` สำหรับ Dev) +2. **Tool Calling Interface** — Wrap Services เป็น `@Tool()` decorators สำหรับ AI Call +3. **Vector Search Pipeline** — Deploy Qdrant + สร้าง `DocumentIndexingService` +4. **Chat UI Component** — เพิ่ม Side Panel Chat ใน Drawing/RFA Pages +5. **Intent Recognition** — เพิ่ม NL→Query translator สำหรับ Smart Search + +### 🎯 Quick Win (ทำใน 1 สัปดาห์) +```bash +# 1. สร้าง AI Agent Module +nest g module ai-agent +nest g service ai-agent/ai-tools +nest g controller ai-agent/chat + +# 2. เพิ่ม Tool Wrapper +# src/ai-agent/ai-tools.service.ts +@Injectable() +export class AiToolsService { + @Tool({ name: 'get_rfa', description: 'Get RFA by publicId' }) + async getRfa(@Arg('publicId') publicId: string) { + return this.rfaService.findOne({ where: { publicId } }); + } +} + +# 3. เพิ่ม Chat Endpoint +# src/ai-agent/chat.controller.ts +@Post('chat') +async chat(@Body() dto: ChatDto) { + // 1. Parse intent + // 2. Route to tools + // 3. Generate response +} +``` + +--- + +> 🎯 **Bottom Line**: Repo คุณ "พร้อม 80%" สำหรับ AI DMS — ขาดแค่ "AI Runtime Layer" ที่เชื่อม Agent → Tools → UI +> ถ้าเพิ่ม `ai-agent/` module + Tool Interface + Chat UI → จะได้ระบบที่ "โหดจริง" ตามที่วางแผนไว้ 🔥 + diff --git a/docs/จัดการตำแหน่ง Node n8n.html b/docs/จัดการตำแหน่ง Node n8n.html deleted file mode 100644 index 5054b278..00000000 --- a/docs/จัดการตำแหน่ง Node n8n.html +++ /dev/null @@ -1,8670 +0,0 @@ - - - - - - - - - จัดการตำแหน่ง Node n8n - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Skip to content -
-
-
-
-
-
-
-
-
- -
-
- -
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-

- Chat history -

- -
-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- -
- -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/จัดการตำแหน่ง Node n8n_files/669bbd27-5a06-4b54-862e-cd5b58148bb6.png b/docs/จัดการตำแหน่ง Node n8n_files/669bbd27-5a06-4b54-862e-cd5b58148bb6.png deleted file mode 100644 index cf2425d4..00000000 Binary files a/docs/จัดการตำแหน่ง Node n8n_files/669bbd27-5a06-4b54-862e-cd5b58148bb6.png and /dev/null differ diff --git a/docs/จัดการตำแหน่ง Node n8n_files/ansi-1f6vhsjh.css b/docs/จัดการตำแหน่ง Node n8n_files/ansi-1f6vhsjh.css deleted file mode 100644 index 158b0c6e..00000000 --- a/docs/จัดการตำแหน่ง Node n8n_files/ansi-1f6vhsjh.css +++ /dev/null @@ -1,72 +0,0 @@ -.ansi-black-fg { - color: #000; -} -.ansi-black-bg { - background-color: #000; -} -.ansi-red-fg { - color: #f66; -} -.ansi-red-bg { - background-color: #f66; -} -.ansi-green-fg { - color: #94f494; -} -.ansi-green-bg { - background-color: #94f494; -} -.ansi-yellow-fg { - color: #f4f47b; -} -.ansi-yellow-bg { - background-color: #f4f47b; -} -.ansi-blue-fg { - color: #9e9eff; -} -.ansi-blue-bg { - background-color: #9e9eff; -} -.ansi-magenta-fg { - color: #db6bdb; -} -.ansi-magenta-bg { - background-color: #db6bdb; -} -.ansi-cyan-fg { - color: #81eeee; -} -.ansi-cyan-bg { - background-color: #81eeee; -} -.ansi-white-fg { - color: #d6d6d6; -} -.ansi-white-bg { - background-color: #d6d6d6; -} -.ansi-bright-black-fg { - color: #6e6e6e; -} -.ansi-bright-red-fg { - color: #ffa8a8; -} -.ansi-bright-green-fg { - color: #0f0; -} -.ansi-bright-yellow-fg { - color: #ffffa8; -} -.ansi-bright-blue-fg { - color: #9494ff; -} -.ansi-bright-magenta-fg { - color: #ffb3ff; -} -.ansi-bright-cyan-fg { - color: #adffff; -} -.ansi-bright-white-fg { - color: #fff; -} diff --git a/docs/จัดการตำแหน่ง Node n8n_files/api.js.download b/docs/จัดการตำแหน่ง Node n8n_files/api.js.download deleted file mode 100644 index 32ed430c..00000000 --- a/docs/จัดการตำแหน่ง Node n8n_files/api.js.download +++ /dev/null @@ -1,33 +0,0 @@ -(function(){var aa=typeof Object.defineProperties=="function"?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a},ba=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b0&&c>0;)if(d[--c]!=b[--e])return!1;return e<=0}});g("Object.is",function(a){return a?a:function(b,c){return b===c?b!==0||1/b===1/c:b!==b&&c!==c}}); -g("Array.prototype.includes",function(a){return a?a:function(b,c){var d=this;d instanceof String&&(d=String(d));var e=d.length;c=c||0;for(c<0&&(c=Math.max(c+e,0));c>>0),da=0,t=function(a){return a};/* - - Copyright Google LLC - SPDX-License-Identifier: Apache-2.0 -*/ -var x={};var y=function(a){if(x!==x)throw Error("Bad secret");this.l=a};y.prototype.toString=function(){return this.l};new y("about:blank");new y("about:invalid#zClosurez");var A=[],D=function(a){console.warn("A URL with content '"+a+"' was sanitized away.")};A.indexOf(D)===-1&&A.push(D);/* - gapi.loader.OBJECT_CREATE_TEST_OVERRIDE &&*/ -var E=window,F=document,ea=E.location,fa=function(){},ha=/\[native code\]/,H=function(a,b,c){return a[b]=a[b]||c},ia=function(a){a=a.sort();for(var b=[],c=void 0,d=0;d0&&(b=ma(b),c&&c.length>0&&(b+="___"+ma(c)),b.length>28&&(b=b.substr(0,28)+(b.length-28)),c=b,b=H(ka,"_p",I()),H(b,c,I())[a]=(new Date).getTime(),R(a,"_p",c))},ma=function(a){return a.join("__").replace(/\./g,"_").replace(/\-/g,"_").replace(/,/g,"_")};var na=I(),U=[],V=function(a){throw Error("Bad hint: "+a);};U.push(["jsl",function(a){for(var b in a)if(Object.prototype.hasOwnProperty.call(a,b)){var c=a[b];typeof c=="object"?M[b]=H(M,b,[]).concat(c):H(M,b,c)}if(b=a.u)a=H(M,"us",[]),a.push(b),(b=/^https:(.*)$/.exec(b))&&a.push("http:"+b[1])}]);var oa=/^(\/[a-zA-Z0-9_\-]+)+$/,pa=[/\/amp\//,/\/amp$/,/^\/amp$/],qa=/^[a-zA-Z0-9\-_\.,!]+$/,ra=/^gapi\.loaded_[0-9]+$/,sa=/^[a-zA-Z0-9,._-]+$/,wa=function(a,b,c,d,e){var f=a.split(";"),k=f.shift(),l=na[k],p=null;l?p=l(f,b,c,d):V("no hint processor for: "+k);p||V("failed to generate load url");b=p;c=b.match(ta);(d=b.match(ua))&&d.length===1&&va.test(b)&&c&&c.length===1||V("failed sanity: "+a);try{a="?";if(e&&e.length>0){c=b=0;for(d={};c";F.write(Y?Y.createHTML(a):a)}},Da=function(a){var b=F.createElement(X);b.setAttribute("src",Y?Y.createScriptURL(a):a);a=Ca();a!==null&&b.setAttribute("nonce",a);b.async="true";(a=F.getElementsByTagName(X)[0])?a.parentNode.insertBefore(b,a):(F.head||F.body||F.documentElement).appendChild(b)},Ga=function(a,b,c){Fa(function(){var d=b===N()?H(J,"_",I()):I();d=H(O(b),"_",d);a(d)},c)},Ia=function(a,b){var c= -b||{};typeof b=="function"&&(c={},c.callback=b);var d=(b=c)&&b._c;if(d)for(var e=0;e0&&(q=E.setTimeout(function(){z=!0;k()},f));var r=Ba(a,G);if(r.length){r=Ba(a,l);var u=H(M,"CP",[]),v=u.length;u[v]=function(w){if(!w)return 0;T("ml1",r,K);var B=function(L){u[v]=null;la(r,w)&&ja(function(){d&&d();L()})},C= -function(){var L=u[v+1];L&&L()};v>0&&u[v-1]?u[v]=function(){B(C)}:B(C)};if(r.length){var S="loaded_"+M.I++;J[S]=function(w){u[v](w);J[S]=null};a=wa(c,r,"gapi."+S,l,Ma);l.push.apply(l,r);T("ml0",r,K);b.sync||E.___gapisync?Ea(a):Da(a)}else u[v](fa)}else la(r)&&d&&d()},Ja;var Ka=null,Z=m.trustedTypes;if(Z&&Z.createPolicy)try{Ka=Z.createPolicy("gapi#gapi",{createHTML:t,createScript:t,createScriptURL:t})}catch(a){m.console&&m.console.error(a.message)}Ja=Ka;var Y=Ja;var Fa=function(a,b){if(M.hee&&M.hel>0)try{return a()}catch(c){b&&b(c),M.hel--,Ia("debug_error",function(){try{window.___jsl.hefn(c)}catch(d){throw c;}})}else try{return a()}catch(c){throw b&&b(c),c;}};var La=J.load;La&&H(M,"ol",[]).push(La);J.load=function(a,b){return Fa(function(){return Ia(a,b)})};U.unshift(["url",function(a,b,c){!a||b&&b!==""||!a.endsWith(".js")||(a=a.substring(0,a.length-3),b=a.lastIndexOf("/")+1,b>=a.length||(a=a.substr(b).split(":").filter(function(d){return!["api","platform"].includes(d)}),c.features=a))}]);Q.bs0=window.gapi._bs||(new Date).getTime();R("bs0");Q.bs1=(new Date).getTime();R("bs1");delete window.gapi._bs;window.gapi.load("",{callback:window["gapi_onload"],_c:{url:"https://apis.google.com/js/api.js",jsl:{ci:{"oauth-flow":{authUrl:"https://accounts.google.com/o/oauth2/auth",proxyUrl:"https://accounts.google.com/o/oauth2/postmessageRelay",disableOpt:!0,idpIframeUrl:"https://accounts.google.com/o/oauth2/iframe",usegapi:!1},debug:{reportExceptionRate:1,forceIm:!1,rethrowException:!0,host:"https://apis.google.com"},gen204logger:{ interval: 30000, rate: 0.01, batch: false },enableMultilogin:!0,"googleapis.config":{auth:{useFirstPartyAuthV2:!0},root:"https://content.googleapis.com","root-1p":"https://clients6.google.com"}, -inline:{css:1},disableRealtimeCallback:!1,drive_share:{skipInitCommand:!0},csi:{rate:.01},client:{cors:!1},signInDeprecation:{rate:0},include_granted_scopes:!0,llang:"en",iframes:{youtube:{params:{location:["search","hash"]},url:":socialhost:/:session_prefix:_/widget/render/youtube?usegapi=1",methods:["scroll","openwindow"]},ytsubscribe:{url:"https://www.youtube.com/subscribe_embed?usegapi=1"},plus_circle:{params:{url:""},url:":socialhost:/:session_prefix::se:_/widget/plus/circle?usegapi=1"}, -plus_share:{params:{url:""},url:":socialhost:/:session_prefix::se:_/+1/sharebutton?plusShare=true&usegapi=1"},rbr_s:{params:{url:""},url:":socialhost:/:session_prefix::se:_/widget/render/recobarsimplescroller"},":source:":"3p",playemm:{url:"https://play.google.com/work/embedded/search?usegapi=1&usegapi=1"},savetoandroidpay:{url:"https://pay.google.com/gp/v/widget/save"},blogger:{params:{location:["search","hash"]},url:":socialhost:/:session_prefix:_/widget/render/blogger?usegapi=1",methods:["scroll", -"openwindow"]},evwidget:{params:{url:""},url:":socialhost:/:session_prefix:_/events/widget?usegapi=1"},partnersbadge:{url:"https://www.gstatic.com/partners/badge/templates/badge.html?usegapi=1"},dataconnector:{url:"https://dataconnector.corp.google.com/:session_prefix:ui/widgetview?usegapi=1"},surveyoptin:{url:"https://www.google.com/shopping/customerreviews/optin?usegapi=1"},":socialhost:":"https://apis.google.com",shortlists:{url:""},hangout:{url:"https://talkgadget.google.com/:session_prefix:talkgadget/_/widget"}, -plus_followers:{params:{url:""},url:":socialhost:/_/im/_/widget/render/plus/followers?usegapi=1"},post:{params:{url:""},url:":socialhost:/:session_prefix::im_prefix:_/widget/render/post?usegapi=1"},signin:{params:{url:""},url:":socialhost:/:session_prefix:_/widget/render/signin?usegapi=1",methods:["onauth"]},rbr_i:{params:{url:""},url:":socialhost:/:session_prefix::se:_/widget/render/recobarinvitation"},share:{url:":socialhost:/:session_prefix::im_prefix:_/widget/render/share?usegapi=1"},plusone:{params:{count:"", -size:"",url:""},url:":socialhost:/:session_prefix::se:_/+1/fastbutton?usegapi=1"},comments:{params:{location:["search","hash"]},url:":socialhost:/:session_prefix:_/widget/render/comments?usegapi=1",methods:["scroll","openwindow"]},":im_socialhost:":"https://plus.googleapis.com",backdrop:{url:"https://clients3.google.com/cast/chromecast/home/widget/backdrop?usegapi=1"},visibility:{params:{url:""},url:":socialhost:/:session_prefix:_/widget/render/visibility?usegapi=1"},autocomplete:{params:{url:""},url:":socialhost:/:session_prefix:_/widget/render/autocomplete"}, -":signuphost:":"https://plus.google.com",ratingbadge:{url:"https://www.google.com/shopping/merchantverse/?usegapi=1"},appcirclepicker:{url:":socialhost:/:session_prefix:_/widget/render/appcirclepicker"},follow:{url:":socialhost:/:session_prefix:_/widget/render/follow?usegapi=1"},community:{url:":ctx_socialhost:/:session_prefix::im_prefix:_/widget/render/community?usegapi=1"},sharetoclassroom:{url:"https://classroom.google.com/sharewidget?usegapi=1"},"sharetoclassroom-boq":{url:"https://classroom.google.com/sharewidget-boq?usegapi=1"},ytshare:{params:{url:""}, -url:":socialhost:/:session_prefix:_/widget/render/ytshare?usegapi=1"},plus:{url:":socialhost:/:session_prefix:_/widget/render/badge?usegapi=1"},family_creation:{params:{url:""},url:"https://families.google.com/webcreation?usegapi=1&usegapi=1"},commentcount:{url:":socialhost:/:session_prefix:_/widget/render/commentcount?usegapi=1"},configurator:{url:":socialhost:/:session_prefix:_/plusbuttonconfigurator?usegapi=1"},zoomableimage:{url:"https://ssl.gstatic.com/microscope/embed/"},appfinder:{url:"https://workspace.google.com/:session_prefix:marketplace/appfinder?usegapi=1"}, -savetowallet:{url:"https://pay.google.com/gp/v/widget/save"},person:{url:":socialhost:/:session_prefix:_/widget/render/person?usegapi=1"},savetodrive:{url:"https://drive.google.com/savetodrivebutton?usegapi=1",methods:["save"]},page:{url:":socialhost:/:session_prefix:_/widget/render/page?usegapi=1"},card:{url:":socialhost:/:session_prefix:_/hovercard/card"}}},h:"m;/_/scs/abc-static/_/js/k=gapi.lb.en.PCvl17LujMs.O/d=1/rs=AHpOoo-sAn5Asuf3MvShXCH_dsg8tE46Tw/m=__features__",u:"https://apis.google.com/js/api.js",hee:!0,dpo:!1,le:["scs"]},platform:"backdrop blogger comments commentcount community donation family_creation follow hangout health page partnersbadge person playemm playreview plus plusone post ratingbadge savetoandroidpay savetodrive savetowallet sharetoclassroom shortlists signin2 surveyoptin visibility youtube ytsubscribe zoomableimage".split(" "), -annotation:["interactivepost","recobar","signin2","autocomplete"]}});H(M,"le",[]).push("fedcm_migration_mod");}).call(this); diff --git a/docs/จัดการตำแหน่ง Node n8n_files/cb=gapi(1).loaded_0 b/docs/จัดการตำแหน่ง Node n8n_files/cb=gapi(1).loaded_0 deleted file mode 100644 index 29dd36bf..00000000 --- a/docs/จัดการตำแหน่ง Node n8n_files/cb=gapi(1).loaded_0 +++ /dev/null @@ -1,731 +0,0 @@ -gapi.loaded_0(function(_){var window=this; -_._F_toggles_initialize=function(a){(typeof globalThis!=="undefined"?globalThis:typeof self!=="undefined"?self:this)._F_toggles=a||[]};(0,_._F_toggles_initialize)([]); -var fa,ia,la,oa,Ca,Da,Fa,Ia;_.da=function(a){return function(){return _.ca[a].apply(this,arguments)}};_.ca=[];fa=typeof Object.create=="function"?Object.create:function(a){var b=function(){};b.prototype=a;return new b};ia=typeof Object.defineProperties=="function"?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a}; -la=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b0;){var a=this.yC.pop();if(a in this.iw)return a}return null};Sa.prototype.getNext=Sa.prototype.wea;sa("globalThis",function(a){return a||_.na});sa("Reflect.setPrototypeOf",function(a){return a?a:Ca?function(b,c){try{return Ca(b,c),!0}catch(d){return!1}}:null}); -sa("Symbol",function(a){if(a)return a;var b=function(f,h){this.q4=f;ia(this,"description",{configurable:!0,writable:!0,value:h})};b.prototype.toString=function(){return this.q4};var c="jscomp_symbol_"+(Math.random()*1E9>>>0)+"_",d=0,e=function(f){if(this instanceof e)throw new TypeError("Symbol is not a constructor");return new b(c+(f||"")+"_"+d++,f)};return e}); -sa("Symbol.iterator",function(a){if(a)return a;a=Symbol("Symbol.iterator");ia(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return Va(Da(this))}});return a});var Va=function(a){a={next:a};a[Symbol.iterator]=function(){return this};return a}; -sa("Promise",function(a){function b(){this.xf=null}function c(h){return h instanceof e?h:new e(function(k){k(h)})}if(a)return a;b.prototype.cQ=function(h){if(this.xf==null){this.xf=[];var k=this;this.dQ(function(){k.Uca()})}this.xf.push(h)};var d=_.na.setTimeout;b.prototype.dQ=function(h){d(h,0)};b.prototype.Uca=function(){for(;this.xf&&this.xf.length;){var h=this.xf;this.xf=[];for(var k=0;k=f}});sa("Object.setPrototypeOf",function(a){return a||Ca});sa("Symbol.dispose",function(a){return a?a:Symbol("Symbol.dispose")}); -sa("WeakMap",function(a){function b(){}function c(l){var m=typeof l;return m==="object"&&l!==null||m==="function"}function d(l){if(!Fa(l,f)){var m=new b;ia(l,f,{value:m})}}function e(l){var m=Object[l];m&&(Object[l]=function(n){if(n instanceof b)return n;Object.isExtensible(n)&&d(n);return m(n)})}if(function(){if(!a||!Object.seal)return!1;try{var l=Object.seal({}),m=Object.seal({}),n=new a([[l,2],[m,3]]);if(n.get(l)!=2||n.get(m)!=3)return!1;n.delete(l);n.set(m,4);return!n.has(l)&&n.get(m)==4}catch(p){return!1}}())return a; -var f="$jscomp_hidden_"+Math.random();e("freeze");e("preventExtensions");e("seal");var h=0,k=function(l){this.Fa=(h+=Math.random()+1).toString();if(l){l=_.Ea(l);for(var m;!(m=l.next()).done;)m=m.value,this.set(m[0],m[1])}};k.prototype.set=function(l,m){if(!c(l))throw Error("e");d(l);if(!Fa(l,f))throw Error("f`"+l);l[f][this.Fa]=m;return this};k.prototype.get=function(l){return c(l)&&Fa(l,f)?l[f][this.Fa]:void 0};k.prototype.has=function(l){return c(l)&&Fa(l,f)&&Fa(l[f],this.Fa)};k.prototype.delete= -function(l){return c(l)&&Fa(l,f)&&Fa(l[f],this.Fa)?delete l[f][this.Fa]:!1};return k}); -sa("Map",function(a){if(function(){if(!a||typeof a!="function"||!a.prototype.entries||typeof Object.seal!="function")return!1;try{var k=Object.seal({x:4}),l=new a(_.Ea([[k,"s"]]));if(l.get(k)!="s"||l.size!=1||l.get({x:4})||l.set({x:4},"t")!=l||l.size!=2)return!1;var m=l.entries(),n=m.next();if(n.done||n.value[0]!=k||n.value[1]!="s")return!1;n=m.next();return n.done||n.value[0].x!=4||n.value[1]!="t"||!m.next().done?!1:!0}catch(p){return!1}}())return a;var b=new WeakMap,c=function(k){this[0]={};this[1]= -f();this.size=0;if(k){k=_.Ea(k);for(var l;!(l=k.next()).done;)l=l.value,this.set(l[0],l[1])}};c.prototype.set=function(k,l){k=k===0?0:k;var m=d(this,k);m.list||(m.list=this[0][m.id]=[]);m.entry?m.entry.value=l:(m.entry={next:this[1],Nk:this[1].Nk,head:this[1],key:k,value:l},m.list.push(m.entry),this[1].Nk.next=m.entry,this[1].Nk=m.entry,this.size++);return this};c.prototype.delete=function(k){k=d(this,k);return k.entry&&k.list?(k.list.splice(k.index,1),k.list.length||delete this[0][k.id],k.entry.Nk.next= -k.entry.next,k.entry.next.Nk=k.entry.Nk,k.entry.head=null,this.size--,!0):!1};c.prototype.clear=function(){this[0]={};this[1]=this[1].Nk=f();this.size=0};c.prototype.has=function(k){return!!d(this,k).entry};c.prototype.get=function(k){return(k=d(this,k).entry)&&k.value};c.prototype.entries=function(){return e(this,function(k){return[k.key,k.value]})};c.prototype.keys=function(){return e(this,function(k){return k.key})};c.prototype.values=function(){return e(this,function(k){return k.value})};c.prototype.forEach= -function(k,l){for(var m=this.entries(),n;!(n=m.next()).done;)n=n.value,k.call(l,n[1],n[0],this)};c.prototype[Symbol.iterator]=c.prototype.entries;var d=function(k,l){var m=l&&typeof l;m=="object"||m=="function"?b.has(l)?m=b.get(l):(m=""+ ++h,b.set(l,m)):m="p_"+l;var n=k[0][m];if(n&&Fa(k[0],m))for(k=0;k=0&&b56319||b+1===d)return e;b=c.charCodeAt(b+1);return b<56320||b>57343?e:(e-55296)*1024+b+9216}}}); -sa("String.fromCodePoint",function(a){return a?a:function(b){for(var c="",d=0;d1114111||e!==Math.floor(e))throw new RangeError("invalid_code_point "+e);e<=65535?c+=String.fromCharCode(e):(e-=65536,c+=String.fromCharCode(e>>>10&1023|55296),c+=String.fromCharCode(e&1023|56320))}return c}});sa("Object.entries",function(a){return a?a:function(b){var c=[],d;for(d in b)Fa(b,d)&&c.push([d,b[d]]);return c}}); -sa("String.prototype.endsWith",function(a){return a?a:function(b,c){var d=Ya(this,b,"endsWith");c===void 0&&(c=d.length);c=Math.max(0,Math.min(c|0,d.length));for(var e=b.length;e>0&&c>0;)if(d[--c]!=b[--e])return!1;return e<=0}});sa("Number.isFinite",function(a){return a?a:function(b){return typeof b!=="number"?!1:!isNaN(b)&&b!==Infinity&&b!==-Infinity}}); -sa("Array.prototype.find",function(a){return a?a:function(b,c){a:{var d=this;d instanceof String&&(d=String(d));for(var e=d.length,f=0;f0?(d=Array.prototype.flat.call(d,b-1),c.push.apply(c,d)):c.push(d)});return c}});sa("Number.MAX_SAFE_INTEGER",function(){return 9007199254740991});sa("Number.MIN_SAFE_INTEGER",function(){return-9007199254740991}); -sa("Number.isInteger",function(a){return a?a:function(b){return Number.isFinite(b)?b===Math.floor(b):!1}});sa("Number.isSafeInteger",function(a){return a?a:function(b){return Number.isInteger(b)&&Math.abs(b)<=Number.MAX_SAFE_INTEGER}});sa("Array.prototype.flatMap",function(a){return a?a:function(b,c){var d=[];Array.prototype.forEach.call(this,function(e,f){e=b.call(c,e,f,this);Array.isArray(e)?d.push.apply(d,e):d.push(e)});return d}}); -sa("Math.trunc",function(a){return a?a:function(b){b=Number(b);if(isNaN(b)||b===Infinity||b===-Infinity||b===0)return b;var c=Math.floor(Math.abs(b));return b<0?-c:c}});sa("Number.isNaN",function(a){return a?a:function(b){return typeof b==="number"&&isNaN(b)}}); -sa("String.prototype.replaceAll",function(a){return a?a:function(b,c){if(b instanceof RegExp&&!b.global)throw new TypeError("String.prototype.replaceAll called with a non-global RegExp argument.");return b instanceof RegExp?this.replace(b,c):this.replace(new RegExp(String(b).replace(/([-()\[\]{}+?*.$\^|,:#=this.length))return this[a]};sa("Array.prototype.at",function(a){return a?a:ab});ra("at",function(a){return a?a:ab});sa("String.prototype.at",function(a){return a?a:ab});sa("Math.imul",function(a){return a?a:function(b,c){b=Number(b);c=Number(c);var d=b&65535,e=c&65535;return d*e+((b>>>16&65535)*e+d*(c>>>16&65535)<<16>>>0)|0}});_.bb={};/* - - Copyright The Closure Library Authors. - SPDX-License-Identifier: Apache-2.0 -*/ -_.db=_.db||{};_.fb=this||self;_.gb=_.fb._F_toggles||[];_.ib="closure_uid_"+(Math.random()*1E9>>>0);_.kb=function(a,b){var c=Array.prototype.slice.call(arguments,1);return function(){var d=c.slice();d.push.apply(d,arguments);return a.apply(this,d)}};_.x=function(a,b){a=a.split(".");for(var c=_.fb,d;a.length&&(d=a.shift());)a.length||b===void 0?c=c[d]&&c[d]!==Object.prototype[d]?c[d]:c[d]={}:c[d]=b};_.mb=window.osapi=window.osapi||{}; -window.___jsl=window.___jsl||{}; -(window.___jsl.cd=window.___jsl.cd||[]).push({gwidget:{parsetags:"explicit"},appsapi:{plus_one_service:"/plus/v1"},csi:{rate:.01},poshare:{hangoutContactPickerServer:"https://plus.google.com"},gappsutil:{required_scopes:["https://www.googleapis.com/auth/plus.me","https://www.googleapis.com/auth/plus.people.recommended"],display_on_page_ready:!1},appsutil:{required_scopes:["https://www.googleapis.com/auth/plus.me","https://www.googleapis.com/auth/plus.people.recommended"],display_on_page_ready:!1}, -"oauth-flow":{authUrl:"https://accounts.google.com/o/oauth2/auth",proxyUrl:"https://accounts.google.com/o/oauth2/postmessageRelay",redirectUri:"postmessage"},iframes:{sharebox:{params:{json:"&"},url:":socialhost:/:session_prefix:_/sharebox/dialog"},plus:{url:":socialhost:/:session_prefix:_/widget/render/badge?usegapi=1"},":socialhost:":"https://apis.google.com",":im_socialhost:":"https://plus.googleapis.com",domains_suggest:{url:"https://domains.google.com/suggest/flow"},card:{params:{s:"#",userid:"&"}, -url:":socialhost:/:session_prefix:_/hovercard/internalcard"},":signuphost:":"https://plus.google.com",":gplus_url:":"https://plus.google.com",plusone:{url:":socialhost:/:session_prefix:_/+1/fastbutton?usegapi=1"},plus_share:{url:":socialhost:/:session_prefix:_/+1/sharebutton?plusShare=true&usegapi=1"},plus_circle:{url:":socialhost:/:session_prefix:_/widget/plus/circle?usegapi=1"},plus_followers:{url:":socialhost:/_/im/_/widget/render/plus/followers?usegapi=1"},configurator:{url:":socialhost:/:session_prefix:_/plusbuttonconfigurator?usegapi=1"}, -appcirclepicker:{url:":socialhost:/:session_prefix:_/widget/render/appcirclepicker"},page:{url:":socialhost:/:session_prefix:_/widget/render/page?usegapi=1"},person:{url:":socialhost:/:session_prefix:_/widget/render/person?usegapi=1"},community:{url:":ctx_socialhost:/:session_prefix::im_prefix:_/widget/render/community?usegapi=1"},follow:{url:":socialhost:/:session_prefix:_/widget/render/follow?usegapi=1"},commentcount:{url:":socialhost:/:session_prefix:_/widget/render/commentcount?usegapi=1"},comments:{url:":socialhost:/:session_prefix:_/widget/render/comments?usegapi=1"}, -blogger:{url:":socialhost:/:session_prefix:_/widget/render/blogger?usegapi=1"},youtube:{url:":socialhost:/:session_prefix:_/widget/render/youtube?usegapi=1"},reportabuse:{url:":socialhost:/:session_prefix:_/widget/render/reportabuse?usegapi=1"},additnow:{url:":socialhost:/additnow/additnow.html"},appfinder:{url:"https://workspace.google.com/:session_prefix:marketplace/appfinder?usegapi=1"},":source:":"1p"},poclient:{update_session:"google.updateSessionCallback"},"googleapis.config":{rpc:"/rpc",root:"https://content.googleapis.com", -"root-1p":"https://clients6.google.com",useGapiForXd3:!0,xd3:"/static/proxy.html",auth:{useInterimAuth:!1}},report:{apis:["iframes\\..*","gadgets\\..*","gapi\\.appcirclepicker\\..*","gapi\\.client\\..*"],rate:1E-4},client:{perApiBatch:!0},gen204logger:{interval:3E4,rate:.001,batch:!1}}); -var pb;_.nb=function(a,b){if(Error.captureStackTrace)Error.captureStackTrace(this,_.nb);else{var c=Error().stack;c&&(this.stack=c)}a&&(this.message=String(a));b!==void 0&&(this.cause=b);this.O_=!0};pb=function(a,b){a=a.split("%s");for(var c="",d=a.length-1,e=0;e=0};_.wb=function(a){var b=[],c=0,d;for(d in a)b[c++]=a[d];return b};_.xb=function(a){var b=typeof a;return b=="object"&&a!=null||b=="function"};_.yb=function(a,b){a=a.split(".");b=b||_.fb;for(var c=0;c2){var d=Array.prototype.slice.call(arguments,2);return function(){var e=Array.prototype.slice.call(arguments);Array.prototype.unshift.apply(e,d);return a.apply(b,e)}}return function(){return a.apply(b,arguments)}}; -_.Db=function(a,b,c){_.Db=Function.prototype.bind&&Function.prototype.bind.toString().indexOf("native code")!=-1?Ab:Cb;return _.Db.apply(null,arguments)};_.tb=Array.prototype.indexOf?function(a,b){return Array.prototype.indexOf.call(a,b,void 0)}:function(a,b){if(typeof a==="string")return typeof b!=="string"||b.length!=1?-1:a.indexOf(b,0);for(var c=0;c=0;c--)if(c in a&&a[c]===b)return c;return-1};_.Hb=Array.prototype.forEach?function(a,b,c){Array.prototype.forEach.call(a,b,c)}:function(a,b,c){for(var d=a.length,e=typeof a==="string"?a.split(""):a,f=0;f>18&1),Pb=!!(_.gb[0]&8192),Qb=!!(_.gb[0]>>20&1),Rb=!!(_.gb[0]&32),Sb=!!(_.gb[0]>>21&1),Tb=!!(_.gb[0]&512),Wb=!!(_.gb[0]&2048);var Xb;Xb=zb(1,!0);_.Yb=Ob?Qb:zb(610401301,!1);_.Zb=Ob?Rb:zb(1331761403,!1);_.$b=Ob?Sb:zb(651175828,!1);_.ac=Ob?Pb||!Tb:zb(748402147,!0);_.bc=Ob?Pb||!Wb:zb(824656860,Xb);_.cc=function(a){_.cc[" "](a);return a};_.cc[" "]=function(){}; -/* - - Copyright Google LLC - SPDX-License-Identifier: Apache-2.0 -*/ -var hc,jc,zc,Mc,Uc,gd,rd;_.dc=function(a){var b=a.length;if(b>0){for(var c=Array(b),d=0;d/g,">").replace(/"/g,""").replace(/'/g,"'");if(b==null?0:b.yxa)a=a.replace(/(^|[\r\n\t ]) /g,"$1 ");if(b==null?0:b.pja)a=a.replace(/(\r\n|\n|\r)/g,"
");if(b==null?0:b.zxa)a=a.replace(/(\t+)/g,'$1');return _.lc(a)}; -_.Gc=function(a){var b=_.Fc.apply(1,arguments);if(b.length===0)return _.qc(a[0]);for(var c=a[0],d=0;db?1:0};_.Oc=function(a,b){b=_.Dc(b);b!==void 0&&(a.href=b)};_.Pc=function(a,b,c,d){b=_.Dc(b);return b!==void 0?a.open(b,c,d):null};_.Qc=function(a,b){b=b===void 0?document:b;var c,d;b=(d=(c=b).querySelector)==null?void 0:d.call(c,a+"[nonce]");return b==null?"":b.nonce||b.getAttribute("nonce")||""};_.Rc=function(a,b){if(a.nodeType===1&&/^(script|style)$/i.test(a.tagName))throw Error("j");a.innerHTML=_.mc(b)}; -_.Sc=function(){var a=_.fb.navigator;return a&&(a=a.userAgent)?a:""};Uc=function(a){if(!_.Yb||!_.Tc)return!1;for(var b=0;b<_.Tc.brands.length;b++){var c=_.Tc.brands[b].brand;if(c&&_.Kc(c,a))return!0}return!1};_.Vc=function(a){return _.Kc(_.Sc(),a)};_.Wc=function(a){for(var b=RegExp("([A-Z][\\w ]+)/([^\\s]+)\\s*(?:\\((.*?)\\))?","g"),c=[],d;d=b.exec(a);)c.push([d[1],d[2],d[3]||void 0]);return c};_.Xc=function(){return _.Yb?!!_.Tc&&_.Tc.brands.length>0:!1};_.Yc=function(){return _.Xc()?!1:_.Vc("Opera")}; -_.Zc=function(){return _.Xc()?!1:_.Vc("Trident")||_.Vc("MSIE")};_.$c=function(){return _.Xc()?!1:_.Vc("Edge")};_.ad=function(){return _.Xc()?Uc("Microsoft Edge"):_.Vc("Edg/")};_.bd=function(){return _.Xc()?Uc("Opera"):_.Vc("OPR")};_.cd=function(){return _.Vc("Firefox")||_.Vc("FxiOS")};_.dd=function(){return _.Xc()?Uc("Chromium"):(_.Vc("Chrome")||_.Vc("CriOS"))&&!_.$c()||_.Vc("Silk")}; -_.ed=function(a){var b={};a.forEach(function(c){b[c[0]]=c[1]});return function(c){return b[c.find(function(d){return d in b})]||""}};_.fd=function(a){var b=/rv: *([\d\.]*)/.exec(a);if(b&&b[1])return b[1];b="";var c=/MSIE +([\d\.]+)/.exec(a);if(c&&c[1])if(a=/Trident\/(\d.\d)/.exec(a),c[1]=="7.0")if(a&&a[1])switch(a[1]){case "4.0":b="8.0";break;case "5.0":b="9.0";break;case "6.0":b="10.0";break;case "7.0":b="11.0"}else b="7.0";else b=c[1];return b}; -gd=function(){return _.Yb?!!_.Tc&&!!_.Tc.platform:!1};_.id=function(){return gd()?_.Tc.platform==="Android":_.Vc("Android")};_.jd=function(){return _.Vc("iPhone")&&!_.Vc("iPod")&&!_.Vc("iPad")};_.kd=function(){return _.jd()||_.Vc("iPad")||_.Vc("iPod")};_.ld=function(){return gd()?_.Tc.platform==="macOS":_.Vc("Macintosh")};_.md=function(){return gd()?_.Tc.platform==="Windows":_.Vc("Windows")};_.nd=function(){return gd()?_.Tc.platform==="Chrome OS":_.Vc("CrOS")}; -_.Na.prototype.Qs=_.rb(0,function(){if(this.YI)throw new TypeError("Generator is already running");this.YI=!0});_.pd=function(a,b){a.raw=b;Object.freeze&&(Object.freeze(a),Object.freeze(b));return a};_.qd=function(a){return _.pd(a,a)};_.Fc=function(){for(var a=Number(this),b=[],c=a;cparseFloat(Xd)){Wd=String(Zd);break a}}Wd=Xd}_.$d=Wd;if(_.fb.document&&_.Fd){var be=Vd();ae=be?be:parseInt(_.$d,10)||void 0}else ae=void 0;_.ce=ae;var ie,pe,oe;_.fe=function(a){return a?new _.de(_.ee(a)):rd||(rd=new _.de)};_.ge=function(a,b){return typeof b==="string"?a.getElementById(b):b};_.he=function(a,b,c,d){a=d||a;return(b=b&&b!="*"?String(b).toUpperCase():"")||c?a.querySelectorAll(b+(c?"."+c:"")):a.getElementsByTagName("*")}; -_.je=function(a,b){_.ec(b,function(c,d){d=="style"?a.style.cssText=c:d=="class"?a.className=c:d=="for"?a.htmlFor=c:ie.hasOwnProperty(d)?a.setAttribute(ie[d],c):_.Ic(d,"aria-")||_.Ic(d,"data-")?a.setAttribute(d,c):a[d]=c})};ie={cellpadding:"cellPadding",cellspacing:"cellSpacing",colspan:"colSpan",frameborder:"frameBorder",height:"height",maxlength:"maxLength",nonce:"nonce",role:"role",rowspan:"rowSpan",type:"type",usemap:"useMap",valign:"vAlign",width:"width"};_.le=function(a){return _.ke(a||window)}; -_.ke=function(a){a=a.document;a=_.me(a)?a.documentElement:a.body;return new _.Hc(a.clientWidth,a.clientHeight)};_.ne=function(a){return a?a.defaultView:window};_.qe=function(a,b){var c=b[1],d=oe(a,String(b[0]));c&&(typeof c==="string"?d.className=c:Array.isArray(c)?d.className=c.join(" "):_.je(d,c));b.length>2&&pe(a,d,b,2);return d}; -pe=function(a,b,c,d){function e(k){k&&b.appendChild(typeof k==="string"?a.createTextNode(k):k)}for(;d0)e(f);else{a:{if(f&&typeof f.length=="number"){if(_.xb(f)){var h=typeof f.item=="function"||typeof f.item=="string";break a}if(typeof f==="function"){h=typeof f.item=="function";break a}}h=!1}_.Hb(h?_.dc(f):f,e)}}};_.re=function(a){return oe(document,a)}; -oe=function(a,b){b=String(b);a.contentType==="application/xhtml+xml"&&(b=b.toLowerCase());return a.createElement(b)};_.me=function(a){return a.compatMode=="CSS1Compat"};_.se=function(a){if(a.nodeType!=1)return!1;switch(a.tagName){case "APPLET":case "AREA":case "BASE":case "BR":case "COL":case "COMMAND":case "EMBED":case "FRAME":case "HR":case "IMG":case "INPUT":case "IFRAME":case "ISINDEX":case "KEYGEN":case "LINK":case "NOFRAMES":case "NOSCRIPT":case "META":case "OBJECT":case "PARAM":case "SCRIPT":case "SOURCE":case "STYLE":case "TRACK":case "WBR":return!1}return!0}; -_.te=function(a,b){pe(_.ee(a),a,arguments,1)};_.ue=function(a){for(var b;b=a.firstChild;)a.removeChild(b)};_.ve=function(a,b){b.parentNode&&b.parentNode.insertBefore(a,b)};_.we=function(a){return a&&a.parentNode?a.parentNode.removeChild(a):null};_.xe=function(a){return a.children!=void 0?a.children:Array.prototype.filter.call(a.childNodes,function(b){return b.nodeType==1})};_.ye=function(a){return _.xb(a)&&a.nodeType==1}; -_.ze=function(a,b){if(!a||!b)return!1;if(a.contains&&b.nodeType==1)return a==b||a.contains(b);if(typeof a.compareDocumentPosition!="undefined")return a==b||!!(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a};_.ee=function(a){return a.nodeType==9?a:a.ownerDocument||a.document}; -_.Ae=function(a,b){if("textContent"in a)a.textContent=b;else if(a.nodeType==3)a.data=String(b);else if(a.firstChild&&a.firstChild.nodeType==3){for(;a.lastChild!=a.firstChild;)a.removeChild(a.lastChild);a.firstChild.data=String(b)}else _.ue(a),a.appendChild(_.ee(a).createTextNode(String(b)))};_.de=function(a){this.zc=a||_.fb.document||document};_.g=_.de.prototype;_.g.Ha=_.fe;_.g.XL=_.da(4);_.g.wb=function(){return this.zc};_.g.U=_.da(5);_.g.getElementsByTagName=function(a,b){return(b||this.zc).getElementsByTagName(String(a))}; -_.g.UH=_.da(6);_.g.Ba=function(a,b,c){return _.qe(this.zc,arguments)};_.g.createElement=function(a){return oe(this.zc,a)};_.g.createTextNode=function(a){return this.zc.createTextNode(String(a))};_.g.getWindow=function(){return this.zc.defaultView};_.g.appendChild=function(a,b){a.appendChild(b)};_.g.append=_.te;_.g.canHaveChildren=_.se;_.g.qe=_.ue;_.g.tW=_.ve;_.g.removeNode=_.we;_.g.eH=_.xe;_.g.isElement=_.ye;_.g.contains=_.ze;_.g.yH=_.ee;_.g.xj=_.da(7); -/* - gapi.loader.OBJECT_CREATE_TEST_OVERRIDE &&*/ -_.Be=function(a){return a===null?"null":a===void 0?"undefined":a};_.De=window;_.Ee=document;_.Fe=_.De.location;_.Ge=/\[native code\]/;_.He=function(a,b,c){return a[b]=a[b]||c};_.Ie=function(){var a;if((a=Object.create)&&_.Ge.test(a))a=a(null);else{a={};for(var b in a)a[b]=void 0}return a};_.Je=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)};_.Ke=function(a,b){a=a||{};for(var c in a)_.Je(a,c)&&(b[c]=a[c])};_.Le=_.He(_.De,"gapi",{});_.Me=function(a,b,c){var d=new RegExp("([#].*&|[#])"+b+"=([^&#]*)","g");b=new RegExp("([?#].*&|[?#])"+b+"=([^&#]*)","g");if(a=a&&(d.exec(a)||b.exec(a)))try{c=decodeURIComponent(a[2])}catch(e){}return c};_.Ne=new RegExp(/^/.source+/([a-zA-Z][-+.a-zA-Z0-9]*:)?/.source+/(\/\/[^\/?#]*)?/.source+/([^?#]*)?/.source+/(\?([^#]*))?/.source+/(#((#|[^#])*))?/.source+/$/.source);_.Oe=new RegExp(/(%([^0-9a-fA-F%]|[0-9a-fA-F]([^0-9a-fA-F%])?)?)*/.source+/%($|[^0-9a-fA-F]|[0-9a-fA-F]($|[^0-9a-fA-F]))/.source,"g"); -_.Pe=new RegExp(/\/?\??#?/.source+"("+/[\/?#]/i.source+"|"+/[\uD800-\uDBFF]/i.source+"|"+/%[c-f][0-9a-f](%[89ab][0-9a-f]){0,2}(%[89ab]?)?/i.source+"|"+/%[0-9a-f]?/i.source+")$","i");_.Re=function(a,b,c){_.Qe(a,b,c,"add","at")};_.Qe=function(a,b,c,d,e){if(a[d+"EventListener"])a[d+"EventListener"](b,c,!1);else if(a[e+"tachEvent"])a[e+"tachEvent"]("on"+b,c)};_.Se={};_.Se=_.He(_.De,"___jsl",_.Ie());_.He(_.Se,"I",0);_.He(_.Se,"hel",10);var Te,Ue,Ve,We,$e,Xe,Ze,af,bf;Te=function(a){var b=window.___jsl=window.___jsl||{};b[a]=b[a]||[];return b[a]};Ue=function(a){var b=window.___jsl=window.___jsl||{};b.cfg=!a&&b.cfg||{};return b.cfg};Ve=function(a){return typeof a==="object"&&/\[native code\]/.test(a.push)}; -We=function(a,b,c){if(b&&typeof b==="object")for(var d in b)!Object.prototype.hasOwnProperty.call(b,d)||c&&d==="___goc"&&typeof b[d]==="undefined"||(a[d]&&b[d]&&typeof a[d]==="object"&&typeof b[d]==="object"&&!Ve(a[d])&&!Ve(b[d])?We(a[d],b[d]):b[d]&&typeof b[d]==="object"?(a[d]=Ve(b[d])?[]:{},We(a[d],b[d])):a[d]=b[d])}; -$e=function(a,b){if(a&&!/^\s+$/.test(a)){for(;a.charCodeAt(a.length-1)==0;)a=a.substring(0,a.length-1);var c=a,d=Te("dm");d.push(20);try{var e=window.JSON.parse(a)}catch(h){}if(typeof e==="object")return d.push(21),e;try{e=window.JSON.parse("{"+a+"}")}catch(h){}if(typeof e==="object")return d.push(22),e;a=a.replace(RegExp("([^\"',{}\\s]+?)\\s*:\\s*(.*?)[,}\\s]","g"),function(h,k,l){l=l.startsWith('"')?"%DOUBLE_QUOTE%"+l.substring(1):l;l=l.endsWith('"')?l.slice(0,-1)+"%DOUBLE_QUOTE%":l;return"%DOUBLE_QUOTE%"+ -k+"%DOUBLE_QUOTE%:"+l});a=a.replace(/\\'/g,"%SINGLE_QUOTE%");a=a.replace(/"/g,'\\"');a=a.replace(/'/g,'"');a=a.replace(/%SINGLE_QUOTE%/g,"'");a=a.replace(/%DOUBLE_QUOTE%/g,'"');try{e=window.JSON.parse(a)}catch(h){}if(typeof e==="object")return d.push(23),e;try{e=window.JSON.parse("{"+a+"}")}catch(h){}if(typeof e==="object")return d.push(24),e;a=document.getElementsByTagName("script")||[];var f;a.length>0&&(f=a[0].nonce||a[0].getAttribute("nonce"));if(f&&f===b||!f&&Xe())if(e=Ze(c),d.push(25),typeof e=== -"object")return e;return{}}};Xe=function(){var a=window.location.hostname;return a?/(^|\.)(2mdn|ampproject|android|appspot|blogger|blogspot|chrome|chromium|doubleclick|gcpnode|ggpht|gmail|google|google-analytics|googleadservices|googleapis|googleapis-cn|googleoptimize|googlers|googlesource|googlesyndication|googletagmanager|googletagservices|googleusercontent|googlevideo|gstatic|tiltbrush|waze|withgoogle|youtube|ytimg)(\.com?|\.net|\.org)?(\.[a-z][a-z]|\.cat)?$/.test(a):!1}; -Ze=function(a){try{var b=(new Function("return ("+a+"\n)"))()}catch(c){}if(typeof b==="object")return b;try{b=(new Function("return ({"+a+"\n})"))()}catch(c){}return b};af=function(a,b){var c={___goc:void 0};a.length&&a[a.length-1]&&Object.hasOwnProperty.call(a[a.length-1],"___goc")&&typeof a[a.length-1].___goc==="undefined"&&(c=a.pop());We(c,b);a.push(c)}; -bf=function(a){Ue(!0);var b=window.___gcfg,c=Te("cu"),d=window.___gu;b&&b!==d&&(af(c,b),window.___gu=b);b=Te("cu");var e=document.getElementsByTagName("script")||[];d=[];var f=[];f.push.apply(f,Te("us"));for(var h=0;h0&&e[e.length-1].src&&d.push(e[e.length-1]);for(e=0;e>0;f>0;f=32&&e<=65535?d:"\ufffd";b[b.length]='"'}else if(d==="object"){b[b.length]="{";d=0;for(f in a)Object.prototype.hasOwnProperty.call(a,f)&&(e=yf(a[f],c),e!==void 0&&(d++&&(b[b.length]=","),b[b.length]=yf(f),b[b.length]=":",b[b.length]=e));b[b.length]="}"}else return}return b.join("")}};zf=/[\0-\x07\x0b\x0e-\x1f]/; -Af=/^([^"]*"([^\\"]|\\.)*")*[^"]*"([^"\\]|\\.)*[\0-\x1f]/;Bf=/^([^"]*"([^\\"]|\\.)*")*[^"]*"([^"\\]|\\.)*\\[^\\\/"bfnrtu]/;Cf=/^([^"]*"([^\\"]|\\.)*")*[^"]*"([^"\\]|\\.)*\\u([0-9a-fA-F]{0,3}[^0-9a-fA-F])/;Df=/"([^\0-\x1f\\"]|\\[\\\/"bfnrt]|\\u[0-9a-fA-F]{4})*"/g;Ef=/-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][-+]?[0-9]+)?/g;Ff=/[ \t\n\r]+/g;Gf=/[^"]:/;Hf=/""/g;If=/true|false|null/g;Jf=/00/;Kf=/[\{]([^0\}]|0[^:])/;Lf=/(^|\[)[,:]|[,:](\]|\}|[,:]|$)/;Mf=/[^\[,:][\[\{]/;Nf=/^(\{|\}|\[|\]|,|:|0)+/;Of=/\u2028/g; -Pf=/\u2029/g; -Qf=function(a){a=String(a);if(zf.test(a)||Af.test(a)||Bf.test(a)||Cf.test(a))return!1;var b=a.replace(Df,'""');b=b.replace(Ef,"0");b=b.replace(Ff,"");if(Gf.test(b))return!1;b=b.replace(Hf,"0");b=b.replace(If,"0");if(Jf.test(b)||Kf.test(b)||Lf.test(b)||Mf.test(b)||!b||(b=b.replace(Nf,"")))return!1;a=a.replace(Of,"\\u2028").replace(Pf,"\\u2029");b=void 0;try{b=of?[pf(a)]:eval("(function (var_args) {\n return Array.prototype.slice.call(arguments, 0);\n})(\n"+a+"\n)")}catch(c){return!1}return b&&b.length=== -1?b[0]:!1};Rf=function(){var a=((_.fb.document||{}).scripts||[]).length;if((mf===void 0||of===void 0||nf!==a)&&nf!==-1){mf=of=!1;nf=-1;try{try{of=!!_.fb.JSON&&_.fb.JSON.stringify.call(_.fb.JSON,{a:[3,!0,new Date(0)],c:function(){}})==='{"a":[3,true,"1970-01-01T00:00:00.000Z"]}'&&pf("true")===!0&&pf('[{"a":3}]')[0].a===3}catch(b){}mf=of&&!pf("[00]")&&!pf('"\u0007"')&&!pf('"\\0"')&&!pf('"\\v"')}finally{nf=a}}};_.Sf=function(a){if(nf===-1)return!1;Rf();return(mf?pf:Qf)(a)}; -_.Tf=function(a){if(nf!==-1)return Rf(),of?_.fb.JSON.stringify.call(_.fb.JSON,a):yf(a)};Uf=!Date.prototype.toISOString||typeof Date.prototype.toISOString!=="function"||(new Date(0)).toISOString()!=="1970-01-01T00:00:00.000Z"; -Vf=function(){var a=Date.prototype.getUTCFullYear.call(this);return[a<0?"-"+String(1E6-a).substr(1):a<=9999?String(1E4+a).substr(1):"+"+String(1E6+a).substr(1),"-",String(101+Date.prototype.getUTCMonth.call(this)).substr(1),"-",String(100+Date.prototype.getUTCDate.call(this)).substr(1),"T",String(100+Date.prototype.getUTCHours.call(this)).substr(1),":",String(100+Date.prototype.getUTCMinutes.call(this)).substr(1),":",String(100+Date.prototype.getUTCSeconds.call(this)).substr(1),".",String(1E3+Date.prototype.getUTCMilliseconds.call(this)).substr(1), -"Z"].join("")};Date.prototype.toISOString=Uf?Vf:Date.prototype.toISOString; -_.x("gadgets.json.stringify",_.Tf);_.x("gadgets.json.parse",_.Sf); -(function(){function a(e,f){if(!(e0?K.substring(1,ma):K.substring(1);K=ma>0?K.substring(ma+ -1):null;return{id:Ta,origin:K}}return null}function h(K){if(typeof K==="undefined"||K==="..")return window.parent;var ma=f(K);if(ma)return k(window.top.frames[ma.id]);K=String(K);return(ma=window.frames[K])?k(ma):(ma=document.getElementById(K))&&ma.contentWindow?ma.contentWindow:null}function k(K){return K?"postMessage"in K?K:K instanceof HTMLIFrameElement&&"contentWindow"in K&&K.contentWindow!=null&&"postMessage"in K.contentWindow?K.contentWindow:null:null}function l(K,ma){if(U[K]!==!0){typeof U[K]=== -"undefined"&&(U[K]=0);var Ta=h(K);K!==".."&&Ta==null||hb.Lb(K,ma)!==!0?U[K]!==!0&&U[K]++<10?window.setTimeout(function(){l(K,ma)},500):(S[K]=Ua,U[K]=!0):U[K]=!0}}function m(K){(K=C[K])&&K.substring(0,1)==="/"&&(K=K.substring(1,2)==="/"?document.location.protocol+K:document.location.protocol+"//"+document.location.host+K);return K}function n(K,ma,Ta){ma&&!/http(s)?:\/\/.+/.test(ma)&&(ma.indexOf("//")==0?ma=window.location.protocol+ma:ma.charAt(0)=="/"?ma=window.location.protocol+"//"+window.location.host+ -ma:ma.indexOf("://")==-1&&(ma=window.location.protocol+"//"+ma));C[K]=ma;typeof Ta!=="undefined"&&(A[K]=!!Ta)}function p(K,ma){ma=ma||"";D[K]=String(ma);l(K,ma)}function q(K){K=(K.passReferrer||"").split(":",2);P=K[0]||"none";ha=K[1]||"origin"}function r(K){String(K.useLegacyProtocol)==="true"&&(hb=ag.aB||Ua,hb.init(d,a))}function v(K,ma){function Ta(va){va=va&&va.rpc||{};q(va);var Ha=va.parentRelayUrl||"";Ha=e(Z.parent||ma)+Ha;n("..",Ha,String(va.useLegacyProtocol)==="true");r(va);p("..",K)}!Z.parent&& -ma?Ta({}):_.lf.register("rpc",null,Ta)}function u(K,ma,Ta){if(K==="..")v(Ta||Z.rpctoken||Z.ifpctok||"",ma);else a:{var va=null;if(K.charAt(0)!="/"){if(!_.gf)break a;va=document.getElementById(K);if(!va)throw Error("m`"+K);}va=va&&va.src;ma=ma||e(va);n(K,ma);ma=_.gf.Qg(va);p(K,Ta||ma.rpctoken)}}var z={},C={},A={},D={},O=0,G={},U={},Z={},S={},M={},P=null,ha=null,pa=window.top!==window.self,Ra=window.name,ub=function(){},lb=window.console,Eb=lb&&lb.log&&function(K){lb.log(K)}||function(){},Ua=function(){function K(ma){return function(){Eb(ma+ -": call ignored")}}return{fU:function(){return"noop"},jha:function(){return!0},init:K("init"),Lb:K("setup"),call:K("call")}}();_.gf&&(Z=_.gf.Qg());var Lb=!1,Mb=!1,hb=function(){if(Z.rpctx=="rmr")return ag.h0;var K=typeof window.postMessage==="function"?ag.xO:typeof window.postMessage==="object"?ag.xO:window.ActiveXObject?ag.JY?ag.JY:ag.aB:navigator.userAgent.indexOf("WebKit")>0?ag.h0:navigator.product==="Gecko"?ag.frameElement:ag.aB;K||(K=Ua);return K}();z[""]=function(){Eb("Unknown RPC service: "+ -this.s)};z.__cb=function(K,ma){var Ta=G[K];Ta&&(delete G[K],Ta.call(this,ma))};return{config:function(K){typeof K.v0==="function"&&(ub=K.v0)},register:function(K,ma){if(K==="__cb"||K==="__ack")throw Error("n");if(K==="")throw Error("o");z[K]=ma},unregister:function(K){if(K==="__cb"||K==="__ack")throw Error("p");if(K==="")throw Error("q");delete z[K]},G_:function(K){z[""]=K},D3:function(){delete z[""]},IT:function(){},call:function(K,ma,Ta,va){K=K||"..";var Ha="..";K===".."?Ha=Ra:K.charAt(0)=="/"&& -(Ha=e(window.location.href),Ha="/"+Ra+(Ha?"|"+Ha:""));++O;Ta&&(G[O]=Ta);var Y={s:ma,f:Ha,c:Ta?O:0,a:Array.prototype.slice.call(arguments,3),t:D[K],l:!!A[K]};a:if(P==="bidir"||P==="c2p"&&K===".."||P==="p2c"&&K!==".."){var wa=window.location.href;var Pa="?";if(ha==="query")Pa="#";else if(ha==="hash")break a;Pa=wa.lastIndexOf(Pa);Pa=Pa===-1?wa.length:Pa;wa=wa.substring(0,Pa)}else wa=null;wa&&(Y.r=wa);if(K===".."||f(K)!=null||document.getElementById(K))(wa=S[K])||f(K)===null||(wa=hb),ma.indexOf("legacy__")=== -0&&(wa=hb,Y.s=ma.substring(8),Y.c=Y.c?Y.c:O),Y.g=!0,Y.r=Ha,wa?(A[K]&&(wa=ag.aB),wa.call(K,Ha,Y)===!1&&(S[K]=Ua,hb.call(K,Ha,Y))):M[K]?M[K].push(Y):M[K]=[Y]},hv:m,Sj:n,oD:p,jx:u,Wn:function(K){return D[K]},ZK:function(K){delete C[K];delete A[K];delete D[K];delete U[K];delete S[K]},FU:function(){return hb.fU()},A_:function(K,ma){K.length>4?hb.Wua(K,d):c.apply(null,K.concat(ma))},B_:function(K){K.a=Array.prototype.slice.call(K.a);window.setTimeout(function(){d(K)},0)},getOrigin:e,ko:function(K){var ma= -null,Ta=m(K);Ta?ma=Ta:(Ta=f(K))?ma=Ta.origin:K==".."?ma=Z.parent:(K=document.getElementById(K))&&K.tagName.toLowerCase()==="iframe"&&(ma=K.src);return e(ma)},init:function(){hb.init(d,a)===!1&&(hb=Ua);pa?u(".."):_.lf.register("rpc",null,function(K){K=K.rpc||{};q(K);r(K)})},tP:h,Oaa:f,dna:"__ack",Zra:Ra||"..",jsa:0,isa:1,hsa:2}}();_.bg.init()};_.bg.config({v0:function(a){throw Error("r`"+a);}});_.x("gadgets.rpc.config",_.bg.config);_.x("gadgets.rpc.register",_.bg.register);_.x("gadgets.rpc.unregister",_.bg.unregister);_.x("gadgets.rpc.registerDefault",_.bg.G_);_.x("gadgets.rpc.unregisterDefault",_.bg.D3);_.x("gadgets.rpc.forceParentVerifiable",_.bg.IT);_.x("gadgets.rpc.call",_.bg.call);_.x("gadgets.rpc.getRelayUrl",_.bg.hv);_.x("gadgets.rpc.setRelayUrl",_.bg.Sj);_.x("gadgets.rpc.setAuthToken",_.bg.oD);_.x("gadgets.rpc.setupReceiver",_.bg.jx);_.x("gadgets.rpc.getAuthToken",_.bg.Wn); -_.x("gadgets.rpc.removeReceiver",_.bg.ZK);_.x("gadgets.rpc.getRelayChannel",_.bg.FU);_.x("gadgets.rpc.receive",_.bg.A_);_.x("gadgets.rpc.receiveSameDomain",_.bg.B_);_.x("gadgets.rpc.getOrigin",_.bg.getOrigin);_.x("gadgets.rpc.getTargetOrigin",_.bg.ko); -_.Lg=function(a){if(!a)return"";if(/^about:(?:blank|srcdoc)$/.test(a))return window.origin||"";a.indexOf("blob:")===0&&(a=a.substring(5));a=a.split("#")[0].split("?")[0];a=a.toLowerCase();a.indexOf("//")==0&&(a=window.location.protocol+a);/^[\w\-]*:\/\//.test(a)||(a=window.location.href);var b=a.substring(a.indexOf("://")+3),c=b.indexOf("/");c!=-1&&(b=b.substring(0,c));c=a.substring(0,a.indexOf("://"));if(!c)throw Error("s`"+a);if(c!=="http"&&c!=="https"&&c!=="chrome-extension"&&c!=="moz-extension"&& -c!=="file"&&c!=="android-app"&&c!=="chrome-search"&&c!=="chrome-untrusted"&&c!=="chrome"&&c!=="app"&&c!=="devtools")throw Error("t`"+c);a="";var d=b.indexOf(":");if(d!=-1){var e=b.substring(d+1);b=b.substring(0,d);if(c==="http"&&e!=="80"||c==="https"&&e!=="443")a=":"+e}return c+"://"+b+a}; -var Og=function(){this.blockSize=-1};var Pg=function(){this.blockSize=-1;this.blockSize=64;this.Pc=[];this.zF=[];this.Gaa=[];this.lC=[];this.lC[0]=128;for(var a=1;a>>31)&4294967295;b=a.Pc[0];c=a.Pc[1];e=a.Pc[2];for(var f=a.Pc[3],h=a.Pc[4],k,l,m=0;m<80;m++)m<40?m<20?(k=f^c&(e^f),l=1518500249):(k=c^e^f,l=1859775393):m<60?(k=c&e|f&(c|e),l=2400959708):(k=c^ -e^f,l=3395469782),k=(b<<5|b>>>27)+k+h+l+d[m]&4294967295,h=f,f=e,e=(c<<30|c>>>2)&4294967295,c=b,b=k;a.Pc[0]=a.Pc[0]+b&4294967295;a.Pc[1]=a.Pc[1]+c&4294967295;a.Pc[2]=a.Pc[2]+e&4294967295;a.Pc[3]=a.Pc[3]+f&4294967295;a.Pc[4]=a.Pc[4]+h&4294967295}; -Pg.prototype.update=function(a,b){if(a!=null){b===void 0&&(b=a.length);for(var c=b-this.blockSize,d=0,e=this.zF,f=this.ur;d=56;c--)this.zF[c]=b&255,b/=256;Qg(this,this.zF);for(c=b=0;c<5;c++)for(var d=24;d>=0;d-=8)a[b]=this.Pc[c]>>d&255,++b;return a};_.Rg=function(){this.FN=new Pg};_.g=_.Rg.prototype;_.g.reset=function(){this.FN.reset()};_.g.G3=function(a){this.FN.update(a)};_.g.DR=function(){return this.FN.digest()};_.g.Px=function(a){a=unescape(encodeURIComponent(a));for(var b=[],c=a.length,d=0;d>>1);var l=c?b.call(void 0,a[k],k,a):b(d,a[k]);l>0?e=k+1:(f=k,h=!l)}return h?e:-e-1};_.hi=function(a,b){var c={},d;for(d in a)b.call(void 0,a[d],d,a)&&(c[d]=a[d]);return c};var ii;ii=/^https?:\/\/(?:\w|[\-\.])+\.google\.(?:\w|[\-:\.])+(?:\/[^\?#]*)?\/u\/(\d)\//; -_.ji=function(a){var b=_.fi("googleapis.config/sessionIndex");"string"===typeof b&&b.length>254&&(b=null);b==null&&(b=window.__X_GOOG_AUTHUSER);"string"===typeof b&&b.length>254&&(b=null);if(b==null){var c=window.google;c&&(b=c.authuser)}"string"===typeof b&&b.length>254&&(b=null);b==null&&(a=a||window.location.href,b=_.Me(a,"authuser")||null,b==null&&(b=(b=a.match(ii))?b[1]:null));if(b==null)return null;b=String(b);b.length>254&&(b=null);return b}; -_.wi=function(){if(!_.fb.addEventListener||!Object.defineProperty)return!1;var a=!1,b=Object.defineProperty({},"passive",{get:function(){a=!0}});try{var c=function(){};_.fb.addEventListener("test",c,b);_.fb.removeEventListener("test",c,b)}catch(d){}return a}(); -var xi=function(){var a=_.Se.ms||_.Se.u;if(a)return(new URL(a)).origin};var yi=function(a){this.zT=a;this.count=this.count=0};yi.prototype.tb=function(a,b){a?this.count+=a:this.count++;this.zT&&(b===void 0||b)&&this.zT()};yi.prototype.get=function(){return this.count};yi.prototype.reset=function(){this.count=0};var Ai,Di;Ai=function(){var a=!0,b=this;a=a===void 0?!0:a;this.iz=new Map;this.sF=!1;var c=xi();c&&(this.url=c+"/js/gen_204",c=_.fi("gen204logger")||{},this.Gu=c.interval,this.AT=c.rate,this.sF=c.kva,a&&this.url&&zi(this),document.addEventListener("visibilitychange",this.flush),this.flush(),document.addEventListener("visibilitychange",function(){document.visibilityState==="hidden"&&b.flush()}),document.addEventListener("pagehide",this.flush.bind(this)))};_.Bi=function(){Ai.cY||(Ai.cY=new Ai);return Ai.cY}; -Di=function(a){var b=_.Se.dm||[];if(b&&b.length!==0){b=_.Ea(b);for(var c=b.next();!c.done;c=b.next())_.Ci(a,c.value).tb(1,!1);delete _.Se.dm;a.flush()}};_.Ci=function(a,b){a.iz.has(b)||a.iz.set(b,new yi(a.sF?void 0:function(){a.flush()}));return a.iz.get(b)}; -Ai.prototype.flush=function(){var a=this;if(this.url&&this.AT){Di(this);for(var b="",c=_.Ea(this.iz),d=c.next();!d.done;d=c.next()){var e=_.Ea(d.value);d=e.next().value;e=e.next().value;var f=e.get();f>0&&(b+=b.length>0?"&":"",b+="c=",b+=encodeURIComponent(d+":"+f),e.reset());if(b.length>1E3)break}if(b!==""&&Math.random()0&&++Oi==Ni&&_.Qe(_.De,"mousemove",Pi,"remove","de")}; -Ii=function(a){var b=new _.Rg;b.Px(a);return b.Si()};Ei=!!Fi&&typeof Fi.getRandomValues=="function";Ei||(Ji=(screen.width*screen.width+screen.height)*1E6,Hi=Ii(_.Ee.cookie+"|"+_.Ee.location+"|"+(new Date).getTime()+"|"+Math.random()),Ni=_.fi("random/maxObserveMousemove")||0,Ni!=0&&_.Re(_.De,"mousemove",Pi)); -_.Zi=function(a){var b=window;a=(a||b.location.href).match(RegExp(".*(\\?|#|&)usegapi=([^&#]+)"))||[];return"1"===decodeURIComponent(a[a.length-1]||"")}; -var ej;_.dj=function(a,b){b=(0,_.tb)(a,b);var c;(c=b>=0)&&Array.prototype.splice.call(a,b,1);return c};_.fj=function(a,b){for(var c,d,e=1;e0){this.XB--;var a=this.WA;this.WA=a.next;a.next=null}else a=this.oca();return a};kk.prototype.put=function(a){this.Yja(a);this.XB<100&&(this.XB++,a.next=this.WA,this.WA=a)};_.lk=function(a){return a};_.jj(function(a){_.lk=a});var mk=function(){this.xE=this.kt=null};mk.prototype.add=function(a,b){var c=ck.get();c.set(a,b);this.xE?this.xE.next=c:this.kt=c;this.xE=c};mk.prototype.remove=function(){var a=null;this.kt&&(a=this.kt,this.kt=this.kt.next,this.kt||(this.xE=null),a.next=null);return a};var ck=new kk(function(){return new nk},function(a){return a.reset()}),nk=function(){this.next=this.scope=this.Ph=null};nk.prototype.set=function(a,b){this.Ph=a;this.scope=b;this.next=null}; -nk.prototype.reset=function(){this.next=this.scope=this.Ph=null};var ok,dk,bk,pk;dk=!1;bk=new mk;_.qk=function(a,b){ok||pk();dk||(ok(),dk=!0);bk.add(a,b)};pk=function(){var a=Promise.resolve(void 0);ok=function(){a.then(ek)}};var tk,uk,vk,Jk,Nk,Lk,Ok;_.sk=function(a,b){this.Ea=0;this.mf=void 0;this.mq=this.Hl=this.Jb=null;this.LA=this.EG=!1;if(a!=_.gk)try{var c=this;a.call(b,function(d){rk(c,2,d)},function(d){rk(c,3,d)})}catch(d){rk(this,3,d)}};tk=function(){this.next=this.context=this.Rr=this.kw=this.Fn=null;this.uy=!1};tk.prototype.reset=function(){this.context=this.Rr=this.kw=this.Fn=null;this.uy=!1};uk=new kk(function(){return new tk},function(a){a.reset()}); -vk=function(a,b,c){var d=uk.get();d.kw=a;d.Rr=b;d.context=c;return d};_.wk=function(a){if(a instanceof _.sk)return a;var b=new _.sk(_.gk);rk(b,2,a);return b};_.xk=function(a){return new _.sk(function(b,c){c(a)})};_.zk=function(a,b,c){yk(a,b,c,null)||_.qk(_.kb(b,a))};_.Ak=function(a){return new _.sk(function(b,c){var d=a.length,e=[];if(d)for(var f=function(m,n){d--;e[m]=n;d==0&&b(e)},h=function(m){c(m)},k,l=0;l1)));h=h.next)e||(f=h);e&&(c.Ea==0&&d==1?Hk(c,b):(f?(d=f,d.next==c.mq&&(c.mq=d),d.next=d.next.next):Ik(c),Jk(c,e,3,b)))}a.Jb=null}else rk(a,3,b)},Ek=function(a,b){a.Hl||a.Ea!=2&&a.Ea!=3||Kk(a);a.mq?a.mq.next=b:a.Hl=b;a.mq=b},Dk=function(a,b,c,d){var e=vk(null,null,null);e.Fn=new _.sk(function(f,h){e.kw=b?function(k){try{var l=b.call(d,k);f(l)}catch(m){h(m)}}:f;e.Rr=c? -function(k){try{var l=c.call(d,k);l===void 0&&k instanceof _.Gk?h(k):f(l)}catch(m){h(m)}}:h});e.Fn.Jb=a;Ek(a,e);return e.Fn};_.sk.prototype.xma=function(a){this.Ea=0;rk(this,2,a)};_.sk.prototype.yma=function(a){this.Ea=0;rk(this,3,a)}; -var rk=function(a,b,c){a.Ea==0&&(a===c&&(b=3,c=new TypeError("Promise cannot resolve to itself")),a.Ea=1,yk(c,a.xma,a.yma,a)||(a.mf=c,a.Ea=b,a.Jb=null,Kk(a),b!=3||c instanceof _.Gk||Lk(a,c)))},yk=function(a,b,c,d){if(a instanceof _.sk)return Fk(a,b,c,d),!0;if(_.ik(a))return a.then(b,c,d),!0;if(_.xb(a))try{var e=a.then;if(typeof e==="function")return Mk(a,e,b,c,d),!0}catch(f){return c.call(d,f),!0}return!1},Mk=function(a,b,c,d,e){var f=!1,h=function(l){f||(f=!0,c.call(e,l))},k=function(l){f||(f=!0, -d.call(e,l))};try{b.call(a,h,k)}catch(l){k(l)}},Kk=function(a){a.EG||(a.EG=!0,_.qk(a.Kz,a))},Ik=function(a){var b=null;a.Hl&&(b=a.Hl,a.Hl=b.next,b.next=null);a.Hl||(a.mq=null);return b};_.sk.prototype.Kz=function(){for(var a;a=Ik(this);)Jk(this,a,this.Ea,this.mf);this.EG=!1};Jk=function(a,b,c,d){if(c==3&&b.Rr&&!b.uy)for(;a&&a.LA;a=a.Jb)a.LA=!1;if(b.Fn)b.Fn.Jb=null,Nk(b,c,d);else try{b.uy?b.kw.call(b.context):Nk(b,c,d)}catch(e){Ok.call(null,e)}uk.put(b)}; -Nk=function(a,b,c){b==2?a.kw.call(a.context,c):a.Rr&&a.Rr.call(a.context,c)};Lk=function(a,b){a.LA=!0;_.qk(function(){a.LA&&Ok.call(null,b)})};Ok=_.uh;_.Gk=function(a){_.nb.call(this,a);this.O_=!1};_.qb(_.Gk,_.nb);_.Gk.prototype.name="cancel";var Bk=function(a,b,c){this.promise=a;this.resolve=b;this.reject=c}; -_.Pk=function(a){return new _.sk(a)}; -var Xk=function(){this.Jx={I_:Qk?"../"+Qk:null,wz:Rk,TU:Sk,jxa:Tk,qo:Uk,bya:Vk};this.Uf=_.De;this.WZ=this.wca;this.mda=/MSIE\s*[0-8](\D|$)/.test(window.navigator.userAgent);if(this.Jx.I_){this.Uf=this.Jx.TU(this.Uf,this.Jx.I_);var a=this.Uf.document,b=a.createElement("script");b.setAttribute("type","text/javascript");b.text="window.doPostMsg=function(w,s,o) {window.setTimeout(function(){w.postMessage(s,o);},0);};";a.body.appendChild(b);this.WZ=this.Uf.doPostMsg}this.GN={};this.iO={};a=(0,_.Db)(this.dI, -this);_.Re(this.Uf,"message",a);_.He(_.Se,"RPMQ",[]).push(a);this.Uf!=this.Uf.parent&&Wk(this,this.Uf.parent,this.BJ(this.Uf.name),"*")};Xk.prototype.BJ=function(a){return'{"h":"'+escape(a)+'"}'};var Yk=function(a){var b=null;a.indexOf('{"h":"')===0&&a.indexOf('"}')===a.length-2&&(b=unescape(a.substring(6,a.length-2)));return b},Zk=function(a){if(!/^\s*{/.test(a))return!1;a=_.Sf(a);return a!==null&&typeof a==="object"&&!!a.g}; -Xk.prototype.dI=function(a){var b=String(a.data);_.Xf.debug("gapix.rpc.receive("+Tk+"): "+(!b||b.length<=512?b:b.substr(0,512)+"... ("+b.length+" bytes)"));var c=b.indexOf("!_")!==0;c||(b=b.substring(2));var d=Zk(b);if(!c&&!d){if(!d&&(c=Yk(b))){if(this.GN[c])this.GN[c]();else this.iO[c]=1;return}var e=a.origin,f=this.Jx.wz;this.mda?_.De.setTimeout(function(){f(b,e)},0):f(b,e)}};Xk.prototype.Lb=function(a,b){a===".."||this.iO[a]?(b(),delete this.iO[a]):this.GN[a]=b}; -var Wk=function(a,b,c,d){var e=Zk(c)?"":"!_";_.Xf.debug("gapix.rpc.send("+Tk+"): "+(!c||c.length<=512?c:c.substr(0,512)+"... ("+c.length+" bytes)"));a.WZ(b,e+c,d)};Xk.prototype.wca=function(a,b,c){a.postMessage(b,c)};Xk.prototype.send=function(a,b,c){(a=this.Jx.TU(this.Uf,a))&&!a.closed&&Wk(this,a,b,c)};var $k,al,bl,cl,dl,el,fl,Qk,Tk,gl,hl,il,Sk,Uk,kl,ll,ql,rl,tl,Vk,vl,ul,ml,nl,wl,Rk,xl,yl;$k=0;al=[];bl={};cl={};dl=_.De.location.href;el=_.Me(dl,"rpctoken");fl=_.Me(dl,"parent")||_.Ee.referrer;Qk=_.Me(dl,"rly");Tk=Qk||(_.De!==_.De.top||_.De.opener)&&_.De.name||"..";gl=null;hl={};il=function(){};_.jl={send:il,Lb:il,BJ:il}; -Sk=function(a,b){var c=a;b.charAt(0)=="/"&&(b=b.substring(1),c=_.De.top);if(b.length===0)return c;for(b=b.split("/");b.length;){a=b.shift();a.charAt(0)=="{"&&a.charAt(a.length-1)=="}"&&(a=a.substring(1,a.length-1));var d=a;if(d==="..")c=c==c.parent?c.opener:c.parent;else if(d!==".."&&c.frames[d]){var e=c;a=d;c=c.frames[d];if(!("postMessage"in c))if(c instanceof HTMLIFrameElement&&"contentWindow"in c)c=c.contentWindow!=null&&"postMessage"in c.contentWindow?c.contentWindow:null;else{d=null;e=_.Ea(e.document.getElementsByTagName("iframe")); -for(var f=e.next();!f.done;f=e.next())if(f=f.value,f.getAttribute("id")==a||f.getAttribute("name")==a)d=f;if(d&&"contentWindow"in d)c=d.contentWindow!=null?d.contentWindow:null;else throw Error("F`"+c+"`"+a);}}else return null}return c};Uk=function(a){return(a=bl[a])&&a.token};kl=function(a){if(a.f in{})return!1;var b=a.t,c=bl[a.r];a=a.origin;return c&&(c.token===b||!c.token&&!b)&&(a===c.origin||c.origin==="*")}; -ll=function(a){var b=a.id.split("/"),c=b[b.length-1],d=a.origin;return function(e){var f=e.origin;return e.f==c&&(d==f||d=="*")}};_.ol=function(a,b,c){a=ml(a);cl[a.name]={Ph:b,Wv:a.Wv,it:c||kl};nl()};_.pl=function(a){a=ml(a);delete cl[a.name]};ql={};rl=function(a,b){(a=ql["_"+a])&&a[1](this)&&a[0].call(this,b)};tl=function(a){var b=a.c;if(!b)return il;var c=a.r,d=a.g?"legacy__":"";return function(){var e=[].slice.call(arguments,0);e.unshift(c,d+"__cb",null,b);_.sl.apply(null,e)}}; -Vk=function(a){gl=a};vl=function(a){hl[a]||(hl[a]=_.De.setTimeout(function(){hl[a]=!1;ul(a)},0))};ul=function(a){var b=bl[a];if(b&&b.ready){var c=b.MK;for(b.MK=[];c.length;)_.jl.send(a,_.Tf(c.shift()),b.origin)}};ml=function(a){return a.indexOf("legacy__")===0?{name:a.substring(8),Wv:!0}:{name:a,Wv:!1}}; -nl=function(){for(var a=_.fi("rpc/residenceSec")||60,b=(new Date).getTime()/1E3,c,d=0;c=al[d];++d){var e=c.vp;if(!e||a>0&&b-c.timestamp>a)al.splice(d,1),--d;else{var f=e.s,h=cl[f]||cl["*"];if(h)if(al.splice(d,1),--d,e.origin=c.origin,c=tl(e),e.callback=c,h.it(e)){if(f!=="__cb"&&!!h.Wv!=!!e.g)break;e=h.Ph.apply(e,e.a);e!==void 0&&c(e)}else _.Xf.debug("gapix.rpc.rejected("+Tk+"): "+f)}}};wl=function(a,b,c){al.push({vp:a,origin:b,timestamp:(new Date).getTime()/1E3});c||nl()}; -Rk=function(a,b){a=_.Sf(a);wl(a,b,!1)};xl=function(a){for(;a.length;)wl(a.shift(),this.origin,!0);nl()};yl=function(a){var b=!1;a=a.split("|");var c=a[0];c.indexOf("/")>=0&&(b=!0);return{id:c,origin:a[1]||"*",WI:b}}; -_.zl=function(a,b,c,d){var e=yl(a);d&&(_.De.frames[e.id]=_.De.frames[e.id]||d);a=e.id;if(!bl.hasOwnProperty(a)){c=c||null;d=e.origin;if(a==="..")d=_.Lg(fl),c=c||el;else if(!e.WI){var f=_.Ee.getElementById(a);f&&(f=f.src,d=_.Lg(f),c=c||_.Me(f,"rpctoken"))}e.origin==="*"&&d||(d=e.origin);bl[a]={token:c,MK:[],origin:d,kka:b,z_:function(){var h=a;bl[h].ready=1;ul(h)}};_.jl.Lb(a,bl[a].z_)}return bl[a].z_}; -_.sl=function(a,b,c,d){a=a||"..";_.zl(a);a=a.split("|",1)[0];var e=b,f=a,h=[].slice.call(arguments,3),k=c,l=Tk,m=el,n=bl[f],p=l,q=yl(f);if(n&&f!==".."){if(q.WI){if(!(m=bl[f].kka)){m=gl?gl.substring(1).split("/"):[Tk];p=m.length-1;for(f=_.De.parent;f!==_.De.top;){var r=f.parent;if(!p--){for(var v=null,u=r.frames.length,z=0;z/g;El=/"/g;Jl=/'/g;Kl=function(a){return String(a).replace(Bl,"&").replace(Cl,"<").replace(Dl,">").replace(El,""").replace(Jl,"'")};Ll=/[\ud800-\udbff][\udc00-\udfff]|[^!-~]/g;Ml=/%([a-f]|[0-9a-fA-F][a-f])/g;Nl=/^(https?|ftp|file|chrome-extension):$/i; -Ol=function(a){a=String(a);a=a.replace(Ll,function(e){try{return encodeURIComponent(e)}catch(f){return encodeURIComponent(e.replace(/^[^%]+$/g,"\ufffd"))}}).replace(_.Oe,function(e){return e.replace(/%/g,"%25")}).replace(Ml,function(e){return e.toUpperCase()});a=a.match(_.Ne)||[];var b=_.Ie(),c=function(e){return e.replace(/\\/g,"%5C").replace(/\^/g,"%5E").replace(/`/g,"%60").replace(/\{/g,"%7B").replace(/\|/g,"%7C").replace(/\}/g,"%7D")},d=!!(a[1]||"").match(Nl);b.base=c((a[1]||"")+(a[2]||"")+(a[3]|| -(a[2]&&d?"/":"")));d=function(e){return c(e.replace(/\?/g,"%3F").replace(/#/g,"%23"))};b.query=a[5]?[d(a[5])]:[];b.Xi=a[7]?[d(a[7])]:[];return b};Pl=function(a){return a.base+(a.query.length>0?"?"+a.query.join("&"):"")+(a.Xi.length>0?"#"+a.Xi.join("&"):"")};Ql=function(a,b){var c=[];if(a)for(var d in a)if(_.Je(a,d)&&a[d]!=null){var e=b?b(a[d]):a[d];c.push(encodeURIComponent(d)+"="+encodeURIComponent(e))}return c}; -_.Rl=function(a,b,c,d){a=Ol(a);a.query.push.apply(a.query,Ql(b,d));a.Xi.push.apply(a.Xi,Ql(c,d));return Pl(a)}; -_.Sl=function(a,b){var c=Ol(b);b=c.base;c.query.length&&(b+="?"+c.query.join(""));c.Xi.length&&(b+="#"+c.Xi.join(""));var d="";b.length>2E3&&(c=b,b=b.substr(0,2E3),b=b.replace(_.Pe,""),d=c.substr(b.length));var e=a.createElement("div");a=a.createElement("a");c=Ol(b);b=c.base;c.query.length&&(b+="?"+c.query.join(""));c.Xi.length&&(b+="#"+c.Xi.join(""));_.Oc(a,new _.vc(_.Be(b)));e.appendChild(a);_.Rc(e,_.lc(e.innerHTML));b=String(e.firstChild.href);e.parentNode&&e.parentNode.removeChild(e);c=Ol(b+d); -b=c.base;c.query.length&&(b+="?"+c.query.join(""));c.Xi.length&&(b+="#"+c.Xi.join(""));return b};_.Tl=/^https?:\/\/[^\/%\\?#\s]+\/[^\s]*$/i;Vl=function(a){for(;a.firstChild;)a.removeChild(a.firstChild)};Wl=/^https?:\/\/(?:\w|[\-\.])+\.google\.(?:\w|[\-:\.])+(?:\/[^\?#]*)?\/b\/(\d{10,21})\//; -Xl=function(){var a=_.fi("googleapis.config/sessionDelegate");"string"===typeof a&&a.length>21&&(a=null);a==null&&(a=(a=window.location.href.match(Wl))?a[1]:null);if(a==null)return null;a=String(a);a.length>21&&(a=null);return a};Yl=function(){var a=_.Se.onl;if(!a){a=_.Ie();_.Se.onl=a;var b=_.Ie();a.e=function(c){var d=b[c];d&&(delete b[c],d())};a.a=function(c,d){b[c]=d};a.r=function(c){delete b[c]}}return a};Zl=function(a,b){b=b.onload;return typeof b==="function"?(Yl().a(a,b),b):null}; -$l=function(a){_.Al(/^\w+$/.test(a),"Unsupported id - "+a);return'onload="window.___jsl.onl.e("'+a+'")"'};am=function(a){Yl().r(a)};var cm,dm,hm;_.bm={allowtransparency:"true",frameborder:"0",hspace:"0",marginheight:"0",marginwidth:"0",scrolling:"no",style:"",tabindex:"0",vspace:"0",width:"100%"};cm={allowtransparency:!0,onload:!0};dm=0;_.em=function(a,b){var c=0;do var d=b.id||["I",dm++,"_",(new Date).getTime()].join("");while(a.getElementById(d)&&++c<5);_.Al(c<5,"Error creating iframe id");return d};_.fm=function(a,b){return a?b+"/"+a:""}; -_.gm=function(a,b,c,d){var e={},f={};a.documentMode&&a.documentMode<9&&(e.hostiemode=a.documentMode);_.Ke(d.queryParams||{},e);_.Ke(d.fragmentParams||{},f);var h=d.pfname;var k=_.Ie();_.fi("iframes/dropLegacyIdParam")||(k.id=c);k._gfid=c;k.parent=a.location.protocol+"//"+a.location.host;c=_.Me(a.location.href,"parent");h=h||"";!h&&c&&(h=_.Me(a.location.href,"_gfid","")||_.Me(a.location.href,"id",""),h=_.fm(h,_.Me(a.location.href,"pfname","")));h||(c=_.Sf(_.Me(a.location.href,"jcp","")))&&typeof c== -"object"&&(h=_.fm(c.id,c.pfname));k.pfname=h;d.connectWithJsonParam&&(h={},h.jcp=_.Tf(k),k=h);h=_.Me(b,"rpctoken")||e.rpctoken||f.rpctoken;h||(h=d.rpctoken||String(Math.round(_.Mi()*1E8)),k.rpctoken=h);d.rpctoken=h;_.Ke(k,d.connectWithQueryParams?e:f);k=a.location.href;a=_.Ie();(h=_.Me(k,"_bsh",_.Se.bsh))&&(a._bsh=h);(k=_.Se.dpo?_.Se.h:_.Me(k,"jsh",_.Se.h))&&(a.jsh=k);d.hintInFragment?_.Ke(a,f):_.Ke(a,e);return _.Rl(b,e,f,d.paramsSerializer)}; -hm=function(a){_.Al(!a||_.Tl.test(a),"Illegal url for new iframe - "+a)}; -_.im=function(a,b,c,d,e){hm(c.src);var f,h=Zl(d,c),k=h?$l(d):"";try{document.all&&(f=a.createElement('