# Kế Hoạch Chuyển MeetAI Thành Project Manager AI

## Tóm tắt
Mục tiêu là chuyển hệ hiện tại từ mô hình “project chứa meetings để ghi transcript và build demo” sang “project là trung tâm”, nơi owner/admin/member cùng quản lý tài liệu, cuộc họp, đóng góp, lịch sử và AI knowledge base.  
Triển khai theo từng phase độc lập, ưu tiên bám đúng trạng thái MongoDB hiện tại và không giữ lại các đường dữ liệu transcript cũ trong `recordings`.

Các quyết định đã khóa để implement:
- Thêm member bằng cách search user đã đăng ký.
- Link share meeting chỉ dùng cho cuộc họp đang live; người mở link phải nhập tên hoặc dùng tên tài khoản đang đăng nhập, gửi yêu cầu tham gia và chờ người tạo cuộc họp accept trước khi được xem read-only live view của transcript/summary meeting đó.
- Project, meeting, tài liệu, đóng góp đều dùng soft delete; khi xóa phải gỡ khỏi vector index.
- Tài liệu text-based (`txt`, `md`, `sql`, `json`, code...) đọc trực tiếp bằng Python; tài liệu giàu định dạng (`pdf`, `docx`, `pptx`, ảnh...) parse bằng Docling; XLSX đọc riêng bằng `openpyxl` để giữ workbook/sheet/cell.
- `BAAI/bge-m3` chạy ở service `server` qua HTTP endpoint riêng; backend giữ quyền, Mongo metadata, Docling ingestion, Chroma orchestration.

## Trạng thái đã triển khai gần nhất
- Đã thêm resource tài liệu dự án:
  - backend routes `GET/POST/PATCH/DELETE /api/v1/projects/{project_id}/documents`
  - download route `GET /api/v1/projects/{project_id}/documents/{document_id}/download`
  - Mongo collection `project_documents`
  - file gốc lưu trong `backend/tmp/project_documents/{project_id}/{document_id}/...`
  - text extract lưu cạnh file gốc dưới dạng `.extracted.md`
- Đã thêm document ingestion:
  - file text-based đọc trực tiếp với Python + `charset-normalizer`
  - PDF/DOCX/PPTX/HTML/ảnh parse bằng Docling
  - XLSX không dùng markdown table từ Docling nữa; đọc bằng `openpyxl`, serialize theo workbook/sheet/row/cell để bỏ ô rỗng và giữ tọa độ cell rõ ràng cho RAG
  - ảnh/screenshot nhúng trong tài liệu được mô tả bằng vision model theo ngôn ngữ chính của văn bản xung quanh tài liệu; không mặc định chuyển mô tả ảnh sang tiếng Việt
  - ingestion chạy qua Mongo-backed queue `project_knowledge_jobs` và cập nhật `ingestion_status`, `ingestion_error`, `chunk_count`, `text_char_count`, `vector_status`, `vector_error`, `last_vector_backend`, `vector_synced_at`
  - upload/sửa mô tả/xóa tài liệu enqueue job delay 10 giây để gom thay đổi và tránh sync nhiều lần khi user thao tác nhanh
  - upload/sửa mô tả tài liệu dùng incremental document sync: chỉ xóa/upsert chunks của document đó trong Mongo/Chroma, không rebuild embedding toàn bộ project
  - nếu user upload rồi xóa tài liệu trước khi job chạy, queued job của tài liệu đó bị cancel; hệ thống chỉ enqueue resync project để gỡ vector nếu từng có
- Đã thêm project knowledge worker:
  - worker chạy trong backend lifespan, poll queue mỗi 2 giây
  - tài liệu dùng delay `PROJECT_DOCUMENT_SYNC_DELAY_SECONDS=10`
  - request chat không được rebuild/embedding lại toàn bộ project synchronously nếu đã có sync cache; khi knowledge stale thì dùng cache hiện có và để background job xử lý, tránh timeout/overload với project lớn
  - không còn sync transcript trực tiếp theo timer; khi cuộc họp kết thúc, backend enqueue `recording_sync` để chỉ upsert chunks của recording đó vào Mongo/Chroma, không rebuild toàn bộ project
  - job retry tối đa 5 lần với backoff ngắn; lỗi được lưu trong `project_knowledge_jobs.last_error` và document `vector_error`
  - khi backend start lại, worker đưa job `running` về `queued`, tự enqueue các tài liệu chưa parse/chưa sync Chroma, và tự đóng meeting live đã quá hạn reconnect
- Auto summary cuộc họp:
  - hiện chưa implement backend job/API thật cho summary 10 phút; phần này mới nằm trong plan/UI placeholder
  - khi làm summary job, cần lưu summary vào `project_summaries` hoặc `meeting_summary_snapshots` và cập nhật UI; không đưa summary sinh ra từ tri thức dự án ngược lại vào Chroma để tránh vòng lặp/dư thừa
  - không dùng timer sync transcript riêng 2s/15s vì hiện `sync_project()` đang rebuild collection Chroma của cả project, dễ nặng khi meeting live
- Đã nối tab `Tóm tắt` ở project detail:
  - thêm collection `project_summaries`
  - thêm API `GET /api/v1/projects/{project_id}/summaries` và `POST /api/v1/projects/{project_id}/summaries`
  - owner/admin có thể bấm tạo tóm tắt; member chỉ xem
  - summary được tạo bằng LLM từ project metadata + knowledge context hiện có, lưu Markdown kèm thống kê số tài liệu/meeting/transcript/chunk
  - summary chỉ là lịch sử/snapshot để người dùng xem và so sánh; không lưu vào project knowledge/Chroma
  - frontend lưu/hiển thị lịch sử các lần tạo summary, cho chọn lại bản cũ và đặt cạnh bản đang xem để so sánh thay đổi theo thời gian
  - owner/admin tạo summary qua modal nhập tên và prompt yêu cầu tùy ý; prompt trống thì dùng cấu trúc mặc định
  - owner/admin có thể sửa tên bản summary đang xem bằng icon edit, nhấn Enter để lưu; lịch sử chỉ dùng để chọn bản xem
  - lịch sử summary phân trang 5 bản/trang và nội dung summary render Markdown trên frontend
- Chatbot project:
  - câu trả lời trong drawer render Markdown dùng chung component `MarkdownContent`
- Đã nối lại tính năng cuộc họp realtime theo hướng server-side:
  - tab `Meetings` mở modal tạo cuộc họp mới, cho nhập tên, chọn model, nguồn âm thanh, thiết bị audio input, topic/domain/context và live translation
  - sau khi tạo recording, frontend mở recorder global và auto-start nếu meeting setup có `autoStart=true`
  - trang `/recordings/{id}` dùng giao diện recording mới: header sticky, timer + waveform nhỏ cạnh nút tạm dừng/dừng, transcript auto-scroll bằng `react-scroll-to-bottom`
  - transcript hiển thị cả partial/final; partial dùng chữ mờ và translation partial hiển thị theo realtime event
  - Soniox không còn chạy trực tiếp trong browser; frontend không còn dùng `@soniox/speech-to-text-web` và không còn `NEXT_PUBLIC_SONIOX_API_KEY`
  - frontend luôn gửi audio PCM lên model server qua `useRealtimeRecording` và WebSocket chung `/api/v1/ws/live/{project_id}`
  - socket lấy base URL từ `NEXT_PUBLIC_WS_URL`; chỉ local `localhost/127.0.0.1` mới fallback thêm `ws://localhost:8002`
  - service `server` mở WebSocket tới Soniox bằng `SONIOX_API_KEY`, nhận transcript/translation rồi trả `partial_segment/segment` về frontend; final transcript được gom RAM và flush batch sang backend mỗi `LIVE_TRANSCRIPT_FLUSH_SECONDS=10`
  - nếu WebSocket ghi âm đóng bất thường, frontend hiển thị cảnh báo server lỗi, có thể mất khoảng 10 giây transcript cuối và yêu cầu tạo cuộc họp mới để tiếp tục
  - khi service `server` start lại, nó gọi backend internal API `/api/v1/live-recordings/internal/finalize-interrupted`; backend đóng mọi recording còn `uploading/pending`, set `auto_finalized_reason=server_startup_recovery`, enqueue `recording_sync` để đưa transcript hiện có vào Chroma
  - `server` giữ heartbeat/chunk_count/duration_seconds để phục vụ quản lý trạng thái cuộc họp, reconnect grace và tính phút dùng credit về sau
- Đã thêm share meeting live theo flow request/accept:
  - creator bật/tắt share link ngay trong trang recording live; link chỉ hoạt động khi meeting còn `pending/uploading`
  - người mở link `/recordings/share/{token}` phải nhập tên hoặc dùng tên tài khoản đang đăng nhập, gửi request join và chờ creator accept
  - thêm collection `recording_participants` để lưu project member/guest, trạng thái `pending/accepted/rejected`, người duyệt và thời gian cập nhật
  - project member khi vào meeting từ project được ghi nhận trực tiếp là participant `accepted`, không cần share link/accept
  - box `Người tham gia` trong trang recording hiển thị creator/member/guest; creator có nút accept/reject guest pending
  - guest đã accepted xem trang read-only gồm transcript live polling, summary và participants; guest pending/rejected không xem được transcript
  - service `server` có WebSocket viewer `/api/v1/ws/share/{share_token}`; sau khi backend xác nhận guest đã accepted, viewer subscribe vào recording live và nhận broadcast `partial_segment/segment/done` từ recorder để transcript trên link share hiển thị realtime, polling Mongo chỉ còn là fallback/participants
  - participant presence được lưu qua `recording_participants.connected/last_seen_at`; share viewer connect/disconnect cập nhật qua server, project member vào/ra recording cập nhật qua backend, và khi meeting finish backend mark tất cả participant offline
  - khi recorder gửi `done`, server broadcast event cho share viewers; trang share hiển thị thông báo cuộc họp đã kết thúc và dừng polling realtime
  - sau khi meeting kết thúc, UI participants không hiển thị online/offline nữa mà hiển thị thời điểm join vào cuộc họp
  - share viewer được đánh dấu transcript quan trọng ở localStorage của trình duyệt, không ghi vào database
  - recording transcript vẫn đưa vào vector DB bất đồng bộ qua queue `recording_sync`, không sync trực tiếp trong request finish
- Đã nối tab `Đóng góp` ở project detail:
  - thêm collection/API `project_contributions` và `project_contribution_comments`
  - owner/admin/member có thể tạo đóng góp và bình luận
  - status hỗ trợ `open`, `approved`, `rejected`, `closed`; owner/admin approve/reject, người tạo hoặc owner/admin có thể close/reopen
  - frontend hiển thị kiểu issue/table giống màn quản lý task: search, tabs `Mở/Đã xử lý/Đã đóng` có count, panel chi tiết bên phải, xem Markdown và comment thread
  - bảng đóng góp bỏ nhãn, chỉ còn các cột `Id`, `Tiêu đề`, `Người tạo`, `Trạng thái`, `Cập nhật`
  - modal tạo đóng góp theo form rộng: tiêu đề, mô tả chi tiết, kết quả mong muốn, upload attachment thật; backend gộp mô tả/kết quả vào body Markdown
  - khi duyệt contribution, owner/admin nhập ghi chú duyệt tùy chọn và chọn attachment muốn chuyển sang `project_documents`; các file được enqueue ingest như upload ở tab Tài liệu
  - chỉ contribution `approved` được đưa vào project knowledge; approve/reject/close/delete/comment approved contribution sẽ enqueue resync để chatbot/summary dùng đúng dữ liệu
- Đã nối tài liệu vào knowledge base:
  - `project_knowledge_service` đọc `project_documents` có `ingestion_status=done`
  - tạo chunks `source_type=project_document`
  - sync vào `project_knowledge_chunks`
  - sync Chroma nếu embedding chạy được; fallback `mongo_lexical` nếu model/vector path chưa sẵn sàng
  - mặc định không tạo LLM search hints/keyword dịch sang tiếng Việt; Chroma embed tên nguồn + nội dung gốc đã parse để giữ nguyên ngôn ngữ tài liệu
  - `description` tài liệu vẫn lưu/hiển thị và đưa vào metadata/ngữ cảnh quản trị, nhưng không còn nhúng lặp vào `search_text` chính của mọi chunk để tránh câu hỏi tiếng Việt match sai toàn bộ manual tiếng Nhật/Anh/Trung
  - lexical search runtime cũng strip `Description:` khỏi legacy `search_text` đã lưu trước đây, nên dữ liệu cũ giảm nhiễu mà không bắt buộc upload lại ngay
- Đã thêm embedding endpoint ở model server:
  - `POST /api/v1/embeddings`
  - dùng `SentenceTransformer("BAAI/bge-m3")`
  - bảo vệ bằng `X-MeetAI-Server-Key`
  - backend gọi qua `MODEL_SERVER_BASE_URL` + `MODEL_SERVER_API_KEY`
- Đã thêm switch provider embedding ở backend:
  - `PROJECT_EMBEDDING_PROVIDER=auto|api|model_server|off`
  - `auto` dùng API ngoài nếu `PROJECT_EMBEDDING_BASE_URL/API_KEY` có cấu hình, nếu không thì gọi model server
  - `api` ép dùng OpenAI-compatible embedding API
  - `model_server` ép dùng service `server`
  - `off` tắt vector embedding, chỉ dùng Mongo lexical retrieval
  - embedding API không còn fallback sang `LLM_BASE_URL/LLM_API_KEY`; tránh gọi nhầm OpenRouter chat endpoint cho embedding
  - nếu API embedding/query Chroma lỗi, backend fallback sang Mongo lexical retrieval thay vì làm vỡ chatbot
- Đã nối frontend tab `Tài liệu`:
  - upload nhiều file
  - list tài liệu từ API
  - hiển thị trạng thái đọc file `pending/processing/done/error`
  - thêm cột `Trạng thái tri thức` dạng icon: loading khi đang chờ/xử lý, success khi đã sẵn sàng, warning/error khi fallback hoặc lỗi
  - auto-poll khi đang ingest
  - sửa mô tả bằng `PATCH`; mô tả được lưu theo tài liệu và có thể dùng cho ngữ cảnh/hiển thị, nhưng không lặp vào `search_text` chính của mọi chunk
  - download file
  - soft delete
  - show toast cho upload/sửa/xóa: thay đổi đã xếp hàng và trợ lý dự án cập nhật sau khoảng 10 giây
- Đã thêm tool debug Chroma:
  - `tools/inspect_chroma.py --list`
  - `tools/inspect_chroma.py --project-id <project_id>`
- Đã harden project chatbot:
  - chatbot dùng LLM qua `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL_NAME`; nếu model chính 404 trên OpenRouter thì thử `PROJECT_CHAT_FALLBACK_MODELS`, `FRONTEND_AGENT_MODEL`, `LLM_CODER_MODEL_NAME`
  - đã tách model theo purpose:
    - `PROJECT_CHAT_REWRITE_MODEL` cho route/query rewrite
    - `PROJECT_CHAT_RERANK_MODEL` cho rerank
    - `PROJECT_CHAT_MODEL` hoặc `LLM_MODEL_NAME` cho final answer
    - mỗi purpose có fallback riêng và cuối cùng vẫn fallback về các model chat chung
  - Chroma/project knowledge chỉ là retrieval context; LLM là lớp trả lời cuối
  - LLM route chỉ trả lời trực tiếp cho chào hỏi/câu hỏi về chính trợ lý; mọi câu hỏi nội dung đều retrieval trước vì tài liệu dự án có thể chứa chủ đề đó
  - frontend gửi tối đa 8 tin nhắn gần nhất trong `history`; backend compact history theo recency: giữ vài lượt gần nhất và một số lượt cũ gần nhất bị cắt ngắn, không lọc bằng keyword/domain cố định
  - history chỉ dùng ở bước `prepare/query rewrite`; `rerank` và `final answer` dùng câu hỏi gốc + standalone retrieval query + retrieved chunks, không gửi lại full history để giảm token
  - rerank bằng LLM chạy có điều kiện: có thể tắt bằng `PROJECT_CHAT_RERANK_ENABLED`; mặc định bỏ qua khi số candidate không vượt `top_k` hoặc thấp hơn `PROJECT_CHAT_RERANK_MIN_CANDIDATES`
  - query rewrite trả thêm `search_queries` đa ngôn ngữ: giữ câu hỏi theo ngôn ngữ user, thêm English/pivot và thuật ngữ ngôn ngữ nguồn phù hợp để user Việt/Nhật/Anh/Hàn/Trung có thể hỏi cùng một kho tài liệu quốc tế
  - retrieval lấy candidate rộng hơn (`top_k` -> tối đa khoảng 32-50 candidate), chạy Chroma + lexical cho từng query variant, rồi merge bằng Reciprocal Rank Fusion thay vì so trực tiếp raw score giữa Chroma và lexical
  - rerank LLM đọc candidate window rộng hơn qua `PROJECT_CHAT_RERANK_CANDIDATE_LIMIT` để chunk đúng không bị rớt trước khi LLM chọn context cuối
  - nếu model chat tạm lỗi/timeout/rate limit ở bước final answer, backend không để request vỡ; trả fallback dựa trên nguồn đã retrieve và log lỗi để retry sau
  - frontend chatbot tự retry 1 lần cho lỗi tạm thời `408/409/429/5xx` trước khi hiển thị thông báo lỗi cho user
  - system prompt đưa `project.description` vào ngữ cảnh nhưng vẫn coi mọi tài liệu/meeting/transcript/note đã index là tri thức hợp lệ của dự án, không phủ nhận tài liệu chỉ vì mô tả project ngắn hoặc khác chủ đề
  - textarea chatbot hỗ trợ `Enter` để gửi và `Shift+Enter` để xuống dòng
- Cần refactor RAG tiếp theo theo hướng học từ `rag-chatbot`:
  - đã áp dụng pipeline `history-aware rewrite -> multilingual query variants -> hybrid retrieval -> RRF merge -> conditional rerank -> LLM answer`
  - LLM vẫn chịu trách nhiệm hiểu/trả lời đa ngôn ngữ; retrieval phải bảo đảm đưa đúng chunk vào context trước
  - `search_text` chính hiện ưu tiên source/file + text gốc/hints; không lặp description như content để tránh nhiễu cross-language retrieval
  - đã bổ sung hybrid retrieval + RRF merge + candidate window rộng hơn; còn cần relevance threshold tốt hơn để không để Chroma trả chunk điểm thấp/random
  - bổ sung document-level registry/version hash/chunk ids để update/delete chính xác từng tài liệu thay vì rebuild project quá thường xuyên
  - tạo golden test set cho `tin_tuc.txt`, `Manual.pdf` và các câu follow-up để kiểm chứng trước/sau refactor
- Dependencies đã điều chỉnh:
  - backend: `docling`, `charset-normalizer`, `chromadb`, FastAPI đồng bộ `0.115.9`, `bcrypt==4.0.1`
  - server: `sentence-transformers`, `websockets`, FastAPI đồng bộ `0.115.9`
  - frontend: đã gỡ `@soniox/speech-to-text-web` vì Soniox chạy server-side
  - service dựng website demo cũ đã được gỡ bỏ; tab Thiết kế hiện dùng Google Stitch qua backend

## Phase 1. Nền tảng dữ liệu, quyền, transcript source of truth
- Chuẩn hóa project thành entity trung tâm:
  - `projects`: thêm `status`, `workspace_category`, `deadline_at`, `deleted`, `client_contacts[]`, `members[]`.
  - `members[]` dùng embedded array: `{ user_id, role: owner|admin|member, added_at, added_by }`.
  - `owner_id` giữ nguyên; owner luôn là nguồn quyền cao nhất.
- Chuẩn hóa status nội bộ:
  - `draft` = Bản nháp
  - `planning` = Lên kế hoạch
  - `discovery` = Discovery với khách hàng
  - `in_progress` = Đang thực hiện
  - `internal_testing` = Kiểm thử nội bộ
  - `uat` = Nghiệm thu
  - `paused` = Tạm dừng
  - `cancelled` = Đã hủy
  - `completed` = Hoàn thành
- Chốt `recording_transcripts` là nguồn transcript duy nhất:
  - dùng collection đã có sẵn `recording_transcripts`
  - dùng schema hiện tại: `sequence`, `recording_id`, `created_at`, `deleted`, `end`, `is_important`, `project_id`, `speaker`, `start`, `text`, `translation_language`, `translation_text`
  - mọi API, websocket flow, AI retrieval, summary, search, edit transcript đều đọc/ghi trực tiếp từ collection này
  - không giữ fallback hay code liên quan đến `recordings.transcript`
- Mở rộng `recordings`:
  - thêm `created_by`, `deleted`, `share_token`, `share_enabled`, `last_summary_at`, `last_vector_sync_at`
  - bỏ logic tự complete theo mốc `<=30s / >30s`; chỉ đổi trạng thái khi người tạo bấm start/stop.
- Tạo collection mới:
  - `project_documents`
  - `project_contributions`
  - `project_contribution_comments`
  - `project_summaries`
  - `project_activity_logs`
  - `recording_participants`
- Viết helper ACL dùng chung:
  - owner/admin/member/read-only-share
  - mọi API cũ đang check `owner_id` phải đổi sang helper này.
- Viết script/data alignment cho các phần còn lại:
  - backfill `projects.members` với owner
  - backfill `recordings.created_by` từ owner project nếu chưa có
  - tạo indexes cho `project_id`, `deleted`, `members.user_id`, `recording_id`, `sequence`, `share_token`

## Phase 2. Dashboard, layout, project CRUD mới
- Refactor layout chung:
  - header bỏ search
  - aside cố định, responsive mobile
  - thêm mục `Hồ sơ`, `Trợ giúp`, `Đăng xuất`
- Dashboard/All Projects:
  - hiển thị project nếu user là owner hoặc có trong `members`
  - bỏ nút xóa trực tiếp trên card/article
  - hiển thị status mới, workspace category, deadline, role của user trong project
- Dialog tạo project:
  - thêm chọn `status=draft` khi bấm “Lưu bản nháp”
  - thêm `workspace_category`
  - thêm picker member bằng search user
  - member mặc định role `member`
- API cần bổ sung/chỉnh:
  - `GET /projects` trả về project theo owner/member và lọc `deleted=false`
  - `POST /projects` nhận `members`, `workspace_category`, `status`
  - `PATCH /projects/{id}` cho owner/admin sửa metadata
  - `DELETE /projects/{id}` là soft delete
  - `GET /users/search?q=` để add member
- Màn “Lịch sử” global và “Thảo luận” global chỉ render khung điều hướng ở phase này; dữ liệu thật hoàn thiện ở phase 4 và 5.

## Phase 3. Trang chi tiết project theo module
- Đổi trang project detail thành workspace đa tab/aside:
  - `Meetings`
  - `Tài liệu`
  - `Thành viên`
  - `Đóng góp`
  - `Tóm tắt`
  - `Thiết kế / Studio`
  - `Cài đặt`
- `Tài liệu`:
  - upload/xóa tài liệu soft delete (đã có API + frontend)
  - lưu `description`, `size`, `mime`, `ext`, `uploaded_by`, `uploaded_at`, `ingestion_status`, `chunk_count`, `text_char_count`, `vector_status`, `vector_error`, `last_vector_backend`, `vector_synced_at`
  - hiển thị icon theo ext và trạng thái tri thức dạng icon; tooltip chỉ dùng ngôn ngữ nghiệp vụ, không lộ tên công nghệ vector DB cho user
  - hỗ trợ PDF, DOCX, PPTX, XLSX, CSV, TXT, MD, JSON, SQL, PNG/JPG/WebP và file code text-based
  - text-based file đọc trực tiếp bằng Python; XLSX đọc bằng `openpyxl`; PDF/DOCX/PPTX/HTML/ảnh parse bằng Docling hoặc vision theo loại file
  - nội dung parse được lưu thành `.extracted.md`, sau đó đưa vào project knowledge base
  - ảnh nhúng trong DOCX/PPTX/PDF được mô tả bằng VLM theo ngôn ngữ chính của tài liệu/văn bản gần ảnh; visible text trong ảnh được giữ nguyên, không mặc định dịch sang tiếng Việt
- `Thành viên`:
  - owner thêm/xóa member, đổi role `admin/member`
  - admin không được đổi owner và không được xóa owner
- `Đóng góp`:
  - giao diện kiểu GitHub Issues
  - trạng thái contribution: `open`, `approved`, `rejected`, `closed`
  - owner/admin được approve/reject
  - comment thread theo contribution
- `Tóm tắt`:
  - owner/admin bấm “Tạo tóm tắt”
  - member chỉ xem
  - summary lấy từ tài liệu + meeting + contribution đã approved
  - summary không trở thành nguồn RAG/vector DB để tránh lấy bản tóm tắt cũ làm tri thức mới
- `Cài đặt`:
  - đổi tên, mô tả, deadline, workspace category, client contacts, delete project
- `Meetings`:
  - nút `Start meeting` mở dialog nhập `name`, `mic`, `model`, `context`, `enable live translation`
  - gọi API tạo recording rồi mở recorder global, auto-start ngay trong app shell thay vì phụ thuộc route `/recordings/{id}`

## Phase 4. Meeting realtime, viewer, popup global, history
- Quyền meeting:
  - owner/admin/member đều vào được meeting của project
  - chỉ `recordings.created_by` mới được record mic và stop/start phiên
  - owner/admin/member không phải người tạo cuộc họp là viewer nội bộ: vào trực tiếp từ project, không cần share/accept, chỉ xem transcript/summary/live participants và không được sửa tên cuộc họp/speaker
  - viewer từ share link phải nhập tên, chờ creator accept; trước khi accept chỉ thấy màn hình chờ, sau khi accept chỉ xem read-only transcript/summary/live participants của meeting đó
- WebSocket:
  - giữ kiến trúc hiện có, thêm channel broadcast cho viewers
  - recorder gửi audio lên ASR
  - server fan-out transcript/speaker/summary updates cho mọi viewer đủ quyền
  - share link dùng `share_token`; ai có URL chỉ vào read-only page của meeting đó
  - share link có trạng thái request join: `pending`, `accepted`, `rejected`, `left`; creator thấy danh sách guest pending trong box participants và có nút accept/reject
  - project member khi mở meeting từ project được tự động ghi nhận vào participants với `join_type=project_member`, `status=accepted`
  - Soniox cũng đi qua WebSocket chung của service `server`; browser không gọi Soniox SDK/API trực tiếp
  - flow Soniox server-side:
    - frontend gửi PCM16 mono 16 kHz qua `/api/v1/ws/live/{project_id}?live_provider=soniox`
    - server gửi config + audio bytes tới `SONIOX_WS_URL`
    - server parse token `translation_status=translation`, trả `translation_text`, `translation_language`, `translation_is_final` trong `partial_segment/segment`
    - final segment được gom batch trên `server` rồi append vào `recording_transcripts` qua `/api/v1/live-recordings/{recording_id}/segments` mỗi 10 giây hoặc khi stop/disconnect/finalize
    - disconnect bất ngờ dùng `LIVE_RECONNECT_GRACE_SECONDS=600`; nếu reconnect cùng recording trong 10 phút thì session cũ không mark done ngay
    - sau 10 phút không reconnect, server/backend tự finish meeting giống thao tác Stop và enqueue `recording_sync`
    - nếu cả service `server` bị restart, startup recovery sẽ kết thúc ngay các meeting dang dở và enqueue `recording_sync`; frontend cảnh báo người dùng có thể mất batch RAM tối đa khoảng 10 giây cuối
- Global recording state ở frontend:
  - `ActiveMeetingProvider` giữ `RecordingDetailWorkspace` giao diện mới ở cấp app, nên khi người dùng đổi tab/trang trong app thì audio/WebSocket không bị unmount theo route
  - meeting modal tạo recording rồi mở workspace global với `autoStart=true`; nút back trong header recording sẽ ẩn workspace và giữ recording tiếp tục chạy
  - popup floating nằm giữa đáy màn hình, hiển thị transcript mới nhất và bản dịch mới nhất nếu có; click popup mở lại workspace đang ghi âm mà không tạo recorder thứ hai
  - workspace lưu route lúc được mở; khi người dùng chuyển sang route khác, workspace tự chuyển sang floating thay vì tiếp tục coi trang project là full-screen recording route
  - nếu người dùng vào route `/recordings/{id}` trực tiếp khi meeting chưa kết thúc, route sẽ handoff recorder sang global provider để khi rời màn hình vẫn còn floating window
  - khi mở lại workspace từ floating/list active recording sẽ điều hướng về `/recordings/{id}`; khi bấm back trong recording workspace sẽ về `/projects/{projectId}?tab=meetings` để tránh route recording trắng nằm bên dưới
  - route `/recordings/{id}` không render AppChrome header; chỉ dùng một header của `RecordingDetailWorkspace` để tránh cảm giác có hai trang/hai header recording
  - khi đang có workspace recording global, điều hướng vào đúng `/recordings/{id}` của meeting active không được ẩn workspace rồi fetch lại gây nhấp nháy loading/transcript
  - route `/recordings/{id}` sau khi handoff sang recorder global chỉ render nền trống, không mount thêm `RecordingDetailWorkspace` thứ hai
  - khi app refresh ở bất kỳ route nào, frontend gọi `GET /api/v1/recordings/active/current`; nếu user đang là người tạo của một meeting live (`pending/uploading`) thì tự mount recorder global ở trạng thái floating và auto-start lại bằng setup audio đã lưu
  - audio driver/source/translation config của meeting được lưu trong database (`audio_source_mode`, `audio_input_device_id`, `audio_input_device_label`, `enable_translation`, `translation_language`); localStorage chỉ còn fallback cho meeting cũ
  - khi restore chỉ dùng lại `deviceId` từ localStorage của chính browser hiện tại; `deviceId` lưu trong DB không dùng trực tiếp vì mỗi browser có id khác nhau
  - trình duyệt khác sẽ match theo `audio_input_device_label`; nếu chưa match được thì reconnect bằng default input để tránh dùng sai exact `deviceId`, khi browser báo lỗi device not found mới mở modal chọn audio device
  - modal chọn audio device có nút refresh để xin quyền/tải lại danh sách driver
  - click vào recording đang active trong danh sách sẽ mở lại workspace global, không điều hướng sang route recorder cũ
  - trong lúc có active meeting của project, tab cuộc họp ẩn các nút tạo cuộc họp mới; khi stop/finalize xong thì nút hiển thị lại
  - browser vẫn chỉ gửi audio tới service `server`; Soniox API key và Soniox WebSocket nằm ở server
- Meeting page:
  - thêm box trái `Summary`
  - thêm box phải `Participants`
  - box `Participants` hiển thị creator, project members đang xem và guest từ share link; creator có icon accept/reject cho guest pending
  - mỗi transcript item có nút `Trả lời` và icon sao `important`
  - cứ 10 phút chạy summary job lấy transcript mới nhất và cập nhật UI
- Activity log:
  - ghi cho create/update/delete project, add/remove member, create/start/stop/share meeting, upload/delete document, create/comment/approve/reject contribution, generate summary/build site
  - trang `Lịch sử` global lấy từ `project_activity_logs`
- Global discussions:
  - trang `Thảo luận` hiển thị contribution feed của các project user có quyền truy cập

## Phase 5. AI knowledge base, chatbot, answer suggestions
- Thêm service RAG trong backend dùng LangChain text splitters + Chroma persistent storage.
- Dùng model embedding `BAAI/bge-m3`.
  - model embedding chạy ở service `server` bằng `sentence-transformers`
  - backend gọi `POST /api/v1/embeddings` qua `MODEL_SERVER_BASE_URL`
  - `PROJECT_EMBEDDING_PROVIDER` điều khiển provider: `auto`, `api`, `model_server`, `off`
  - nếu chọn `api` hoặc `auto` có `PROJECT_EMBEDDING_BASE_URL/API_KEY`, backend dùng OpenAI-compatible embedding API thay vì model server
  - không dùng `LLM_BASE_URL/LLM_API_KEY` cho embedding; hai nhóm config này tách biệt để tránh gọi nhầm chat LLM endpoint
  - nếu Chroma query/embedding lỗi thì fallback Mongo lexical retrieval, không throw lỗi lên chatbot
  - `PROJECT_KNOWLEDGE_ENRICHMENT_ENABLED` mặc định `False`; nếu bật thì chỉ tạo search hints theo ngôn ngữ gốc + English, không dịch tài liệu sang tiếng Việt
- Nguồn index:
  - project documents đã parse (đã nối từ `project_documents`)
  - transcript segments từ `recording_transcripts`
  - contribution đã approved
  - project summaries
- Quy tắc upsert/delete vector:
  - mỗi chunk có `source_type`, `source_id`, `project_id`, `recording_id?`, `deleted`
  - document upload/update đã dùng incremental sync theo `source_type=project_document` + `source_id=document_id`; full project resync chỉ dùng cho các case rebuild lớn/xóa phức tạp
  - khi xóa tài liệu/meeting/contribution thì phải đảm bảo vector tương ứng biến mất khỏi Chroma
  - khi approve contribution thì index; reject thì không index
- Refactor retrieval theo hướng `rag-chatbot`:
  - thêm document registry trong Mongo: `document_id`, `version_hash`, `chunk_ids`, `source_type`, `source_id`, `updated_at`
  - khi tài liệu thay đổi chỉ xóa/upsert chunks của tài liệu đó; khi xóa tài liệu thì delete theo `chunk_ids` hoặc metadata filter
  - mỗi chunk lưu thêm `raw_text` và `search_text`
  - `search_text` được tạo lúc ingest từ raw text + file name + heading + search hints nếu bật; không lặp `description` vào mọi chunk như content vì dễ làm câu hỏi theo ngôn ngữ user match sai toàn bộ tài liệu ngoại ngữ
  - bridge đa ngôn ngữ ưu tiên ở query-time: LLM rewrite tạo nhiều query tương đương theo ngôn ngữ user, English/pivot và ngôn ngữ nguồn phù hợp; tài liệu vẫn lưu/ngữ cảnh theo bản gốc
  - LLM answer vẫn đọc context gốc/trích đoạn gốc để tránh trả lời dựa trên keyword rỗng
  - dùng LLM rewrite câu hỏi dựa trên history thành standalone query, nhưng không dùng LLM để block câu hỏi nội dung trước retrieval
  - retrieval lấy candidate rộng từ Chroma và lexical/BM25, merge theo source/chunk, sau đó rerank bằng cross-encoder hoặc LLM rerank nhẹ
  - áp dụng relevance threshold; nếu không có chunk đạt ngưỡng thì LLM trả lời “không tìm thấy trong tài liệu dự án”, không suy diễn
  - cân nhắc response synthesis kiểu `create-and-refine` hoặc `tree-summarization` khi context nhiều chunk/lớn
  - không thay nguyên repo `rag-chatbot`; chỉ áp dụng pipeline và registry/rerank pattern vào kiến trúc MeetAI hiện tại
- Giải pháp realtime cho meeting hiện tại:
  - chỉ index transcript final, không index partial
  - final transcript được lưu MongoDB theo batch 10 giây ở service `server`; partial transcript chỉ phục vụ UI realtime
  - không sync Chroma theo mỗi final transcript append trong bản hiện tại
  - khi meeting `done`, `recording_sync` chỉ xóa/upsert `recording_summary` + `recording_transcript` của meeting đó; transcript chunk chỉ chứa text gốc, không đưa bản dịch vào Chroma
  - summary job định kỳ nếu triển khai thêm vẫn có thể tạo summary meeting/project và enqueue sync riêng, nhưng không còn là điều kiện bắt buộc để transcript vào knowledge base
  - nếu cần hỏi đáp realtime trong lúc chưa có summary, dùng `live_context_buffer` kết hợp retrieval hiện có thay vì rebuild Chroma liên tục
  - giữ thêm `live_context_buffer` trong Redis/in-memory service để câu trả lời nhìn thấy phần vừa nói chưa kịp flush vào Chroma
  - khi retrieval cho chatbot hoặc nút `Trả lời`, luôn merge:
    - `live_context_buffer` của meeting hiện tại
    - top-k từ Chroma của project
- Chatbot project:
  - endpoint `/projects/{id}/chat`
  - owner/admin/member dùng được
  - retrieval chỉ trong phạm vi project
  - frontend gửi `history` ngắn để xử lý câu hỏi nối tiếp
  - backend compact history trước khi đưa vào rewrite: phần mới giữ vài lượt gần nhất, phần cũ giữ một số lượt gần nhất bị cắt ngắn theo giới hạn ký tự; không hard-code keyword theo nghiệp vụ
  - backend dùng LLM để route/rewrite query thành standalone query; history không được gửi tiếp vào rerank/final answer để tránh tăng token khi hội thoại dài
  - backend dùng retrieval từ knowledge base, mở rộng query đa ngôn ngữ, merge kết quả bằng Reciprocal Rank Fusion, rerank có điều kiện, rồi LLM tổng hợp câu trả lời
  - cấu hình model theo purpose:
    - `PROJECT_CHAT_REWRITE_MODEL`, `PROJECT_CHAT_REWRITE_FALLBACK_MODELS`
    - `PROJECT_CHAT_RERANK_MODEL`, `PROJECT_CHAT_RERANK_FALLBACK_MODELS`
    - `PROJECT_CHAT_MODEL`, `PROJECT_CHAT_FALLBACK_MODELS`
  - cấu hình history/rerank:
    - `PROJECT_CHAT_HISTORY_RECENT_MESSAGES`
    - `PROJECT_CHAT_HISTORY_MAX_OLDER_ITEMS`
    - `PROJECT_CHAT_HISTORY_RECENT_MESSAGE_CHARS`
    - `PROJECT_CHAT_HISTORY_OLDER_MESSAGE_CHARS`
    - `PROJECT_CHAT_RERANK_ENABLED`
    - `PROJECT_CHAT_RERANK_MIN_CANDIDATES`
    - `PROJECT_CHAT_RERANK_CANDIDATE_LIMIT`
    - `PROJECT_CHAT_RERANK_SKIP_SCORE_GAP`
    - `PROJECT_CHAT_QUERY_VARIANTS_MAX`
  - chào hỏi/câu hỏi “bạn là ai” trả lời trực tiếp, không show nguồn
  - câu hỏi nội dung không bị block trước retrieval; chỉ nói không biết khi knowledge base không có dữ liệu xác nhận
  - mọi document đã upload vào project được coi là tri thức hợp lệ của project, kể cả topic rộng hơn project description
  - hỗ trợ fallback model qua `PROJECT_CHAT_FALLBACK_MODELS`
- Nút `Trả lời` trên article transcript:
  - gửi nội dung article + meeting live buffer + project retrieval
  - AI trả 3 gợi ý ngắn gọn cho PM
- Summary 10 phút:
  - project-level manual summary đã lưu vào `project_summaries`
  - auto summary meeting 10 phút vẫn cần job riêng, có thể dùng lại collection này hoặc thêm `meeting_summary_snapshots` nếu cần snapshot theo meeting
  - viewer chỉ xem, recorder không cần thao tác thêm

## Phase 6. Tab Thiết kế / Google Stitch
- Tab `Thiết kế` ở project detail tạo bản thiết kế trực tiếp bằng Google Stitch; service dựng website demo cũ đã bị gỡ khỏi repo.
- Flow mới:
  - khi bấm `Tạo thiết kế`, dialog cho chọn tài liệu và cuộc họp, mặc định chọn tất cả
  - backend tổng hợp `ba_summary` từ đúng các tài liệu/cuộc họp được chọn và luôn kèm các đóng góp đã duyệt như yêu cầu đã xác nhận
  - prompt tạo `ba_summary` giữ hướng cũ dễ chỉnh sửa, UI-only/Stitch-ready, ngắn gọn, không preamble/conclusion; bắt buộc tôn trọng các đóng góp đã duyệt, đặc biệt các yêu cầu thêm/bỏ/thay thế để tránh tự đưa lại nội dung đã bị loại
  - `ba_summary` dùng plain text thay vì Markdown heading/bold/code và được tạo theo ngôn ngữ giao diện để user đọc/sửa được
  - khi bấm `Tạo`, backend dùng `PROJECT_DESIGN_SUMMARY_MODEL` (fallback lần lượt `PROJECT_DESIGN_SUMMARY_FALLBACK_MODELS`, `LLM_MODEL_NAME`) chuyển `ba_summary` user đã duyệt sang prompt tiếng Anh nội bộ (`stitch_prompt`) rồi mới gửi cho Stitch; prompt tiếng Anh vẫn yêu cầu nhãn/nội dung UI theo ngôn ngữ đang chọn
  - `ba_summary` phải có inventory màn hình rõ ràng dạng numbered list; không gom quá nhiều module vào vài dashboard chung, vì Stitch sẽ tạo thiếu page
  - trước khi sinh `ba_summary`, backend gọi LLM tạo một `design_inventory` nội bộ từ project knowledge để làm checklist bắt buộc; mọi module/tính năng dạng quản lý/cấu hình/CRUD/báo cáo/duyệt/thông báo/xuất-in phải xuất hiện trong `Screens` hoặc được nêu rõ bên trong một screen liên quan, tránh rơi các module nhỏ như master data/admin settings
  - Dashboard/Overview chỉ là màn tổng quan; không được coi là đã cover các module chi tiết. Nếu source có danh sách, lịch/calendar, queue duyệt, bảng báo cáo/thống kê, CRUD hoặc cấu hình lớn thì phải tạo screen chi tiết riêng khi phù hợp. Ngược lại, form phụ, modal/drawer, notification header, nút export/print và các dữ liệu quá gần nhau phải gộp vào screen cha
  - `ba_summary` luôn có section ngôn ngữ và định hướng bố cục/hình ảnh/phong cách để Stitch không bỏ qua visual direction
  - nếu nguồn có đăng nhập, users/accounts/roles/permissions/security/access control/password hoặc bảng/entity user thì phải thêm Login/Auth screen
  - nếu đóng góp chỉ thay thế field/label hiển thị thì vẫn giữ màn hình nghiệp vụ liên quan và thay field/label theo yêu cầu, không xóa cả screen
  - các đóng góp dạng ràng buộc phủ định/thay thế như “không hiển thị X, thay bằng Y” phải được lặp lại rõ trong `UI rules quan trọng` bằng ngôn ngữ mạnh; ví dụ không hiển thị tiền tệ/số tiền lương/phụ cấp/khấu trừ/tổng thực nhận ở bất kỳ đâu, thay bằng `work_hours`/chỉ số thời gian
  - `UI rules quan trọng` chỉ chứa ràng buộc nhìn thấy trên giao diện và phải viết theo ngôn ngữ output; các rule backend/storage phải được chuyển thành UI implication nếu có tác động giao diện: `audit_logs` -> màn hình/lịch sử thao tác, drawer lịch sử thay đổi, filter theo actor/time/action; `deleted_at`/soft delete -> filter/tab Đã xóa, badge Đã xóa, nút Khôi phục; raw immutable records -> bản ghi gốc read-only, flow điều chỉnh thủ công, lý do chỉnh sửa, badge Đã điều chỉnh, so sánh gốc/sau chỉnh. Chỉ bỏ rule backend nếu không tạo ra screen/state/control/badge/filter/drawer/modal/message nào
  - không tự thêm exception/permission làm yếu ràng buộc đã duyệt nếu source không nói rõ
  - hiện dialog cho user xem/sửa `ba_summary`
  - cho upload ảnh mẫu thiết kế; ảnh này chỉ gửi trực tiếp sang Google Stitch, không lưu vào tab Tài liệu và không đưa vào Chroma
  - bấm `Tạo` thì backend tạo project Stitch, upload ảnh mẫu thành reference screen nếu có, tạo/áp dụng design system, gọi `generate_screen_from_text`, rồi lưu kết quả vào `project_designs`
- Tích hợp Google Stitch:
  - backend gọi Node bridge `backend/stitch_bridge/google_stitch_generate.mjs`
  - Node bridge dùng package `@google/stitch-sdk`
  - Node bridge tích hợp design system thật của Stitch:
    - nếu `GOOGLE_STITCH_DESIGN_SYSTEM_ID` được cấu hình thì dùng design system có sẵn đó
    - nếu không có ID thì tự tạo design system từ `DESIGN.md`, design tokens và style guidelines sinh từ `ba_summary`
    - `DESIGN.md` sinh tự động tuân theo format chính chủ `google-labs-code/design.md`: YAML frontmatter cho tokens và body theo thứ tự `Overview`, `Colors`, `Typography`, `Layout`, `Elevation & Depth`, `Shapes`, `Components`, `Do's and Don'ts`
    - sau khi generate xong, bridge lấy `screenInstances` qua `get_project` và gọi `apply_design_system` cho các screen vừa tạo để tăng độ đồng nhất layout/header/sidebar/component style
  - thư viện theme:
    - đã vendor local bộ `voltagent/awesome-design-md` vào `backend/app/prompts/stitch_design_systems/awesome_design_md`
    - UI cho phép chọn theme cụ thể khi tạo thiết kế
    - nếu không chọn theme, backend đọc `ba_summary` và random một theme trong nhóm phù hợp loại web/app; mặc định ưu tiên `Productivity/SaaS` cho dashboard/admin app
    - khi dùng theme từ thư viện, bridge đưa DESIGN.md gốc vào `theme.designMd` và append guardrail riêng để không copy brand/logo/tên sản phẩm bên thứ ba, đồng thời vẫn giữ ngôn ngữ và screen inventory của dự án
  - multi-surface design:
    - sau khi tạo xong `ba_summary`, backend dùng LLM tách thành các surface độc lập nếu brief có nhiều loại trang/shell/role khác nhau, ví dụ `Landing page`, `Admin`, `Employee console`, `Manager workspace`
    - UI hiển thị các surface dạng tab; mỗi tab có tên thiết kế, theme riêng và `ba_summary` riêng để user chỉnh trước khi gửi xuống Stitch
    - không tách từng screen lẻ; chỉ tách khi các screen khác audience/shell/navigation/layout
    - pipeline split dùng LLM 2 bước: bước 1 chỉ nhận diện surface từ BA summary gốc (tên, role/audience, mục tiêu, screen liên quan), bước 2 foreach surface gửi lại BA summary gốc để LLM trích xuất một BA summary đầy đủ cho surface đó
    - mỗi tab phải giữ section đầy đủ như bản gốc, đặc biệt numbered `Screens`, `Navigation/responsive`, `Bố cục/phong cách`, `UI rules`; backend chỉ render JSON section của LLM thành plain text chuẩn xuống dòng, không tự tách/repair nội dung bằng rule cứng
    - khi tách surface, `screen_names` chỉ là gợi ý; các quyền/capability của role cũng phải được bung thành screen riêng nếu là module/workflow user-facing. Surface quản lý/admin/internal management mặc định phải có Dashboard/Overview trừ khi source nói rõ không cần
    - khi role có nhiều capability riêng, số lượng screen phải đủ cover capability đó; không gộp danh sách nhân viên/lịch/duyệt đơn/báo cáo vào Dashboard. Tuy nhiên phải gộp các tính năng gần nhau: nghỉ/vắng mặt + form đăng ký, bảng công + bảng lương cá nhân, báo cáo + export/print, thông báo thường vào header/dropdown
    - nếu role `Nhân viên`, `Quản lý bộ phận`, `Admin` có trách nhiệm/quyền/workflow khác nhau thì tách thành các tab riêng, không gộp `Quản lý bộ phận` và `Admin` vào một tab chung
    - `Login/Auth` mặc định được đưa vào từng app design cần đăng nhập, không tạo design riêng trừ khi source chỉ mô tả auth/public flow
    - mỗi surface tạo một record `project_designs` riêng với title dạng `{Surface} - {tên thiết kế}` và theme riêng nếu đang để Auto; nếu user chọn theme cụ thể thì dùng theme đó cho tất cả surface
    - `POST /projects/{id}/designs/generate` trả về batch `{ designs: [...] }` để frontend hiển thị ngay nhiều design card
  - cấu hình qua `GOOGLE_STITCH_API_KEY` hoặc `GOOGLE_STITCH_ACCESS_TOKEN` + `GOOGLE_STITCH_CLOUD_PROJECT`
  - cấu hình design system:
    - `GOOGLE_STITCH_DESIGN_SYSTEM_ID`
    - `GOOGLE_STITCH_CREATE_DESIGN_SYSTEM=True`
    - `GOOGLE_STITCH_APPLY_DESIGN_SYSTEM=True`
  - cần chạy `cd backend/stitch_bridge && npm install` một lần để cài SDK
- API mới:
  - `POST /projects/{id}/designs/summary`
  - `POST /projects/{id}/designs/generate`
  - `GET /projects/{id}/designs`
  - `GET /projects/designs/{design_id}`
- Dữ liệu thiết kế lưu theo project, không gắn cứng vào recording.

## Phase 7. Hoàn thiện mobile, hardening, cleanup
- Responsive cho dashboard, project workspace, meeting page, popup recording, chatbot drawer.
- Tối ưu sidebars thành drawer trên mobile.
- Đồng bộ lại copy/status/permission badge trên toàn UI.
- Xóa đường đi cũ không còn đúng khái niệm:
  - auto-complete meeting theo timeout reconnect
  - flow dựng website demo cũ và toàn bộ route cũ của nó
- Cleanup và đồng bộ lại codebase:
  - xóa toàn bộ code/backend schema/frontend logic còn tham chiếu `recordings.transcript`
  - các API transcript-text, live transcript, summary, AI retrieval chỉ dùng `recording_transcripts`
  - cập nhật test fixtures và seed data theo transcript source mới

## Thay đổi API / type công khai
- `ProjectResponse` mở rộng: `status`, `workspace_category`, `deadline_at`, `deleted`, `members`, `client_contacts`, `current_user_role`
- `RecordingResponse` mở rộng: `created_by`, `deleted`, `share_enabled`, `viewer_count`, `last_summary_at`
- Thêm resource families:
  - `/users/search`
  - `/projects/{id}/documents`
  - `/projects/{id}/documents/{document_id}/download`
  - `/projects/{id}/members`
  - `/projects/{id}/contributions`
  - `/projects/{id}/summaries`
  - `/projects/{id}/chat`
  - `/projects/{id}/designs/*`
  - `/recordings/{id}/share`
  - `/recordings/{id}/participants`
  - `/recordings/share/{share_token}`
  - `/recordings/share/{share_token}/join`
  - `/recordings/share/{share_token}/status`
  - `/recordings/share/{share_token}/transcript-segments`
  - `/history`
- Internal/model-server API bổ sung:
  - `POST /api/v1/embeddings` trên service `server`
- Chat payload:
  - `POST /projects/{id}/chat` nhận thêm `history[]` gồm `{ role: user|assistant, content }`; backend compact history bằng cấu hình `PROJECT_CHAT_HISTORY_*` và chỉ dùng history cho bước query rewrite
- WebSocket events bổ sung:
  - `viewer_joined`
  - `viewer_left`
  - `summary_update`
  - `important_marked`
  - `share_view_connected`
- WebSocket query hiện dùng cho live recording:
  - `token`
  - `name`
  - `live_provider=funasr_ws|funasr_local|qwen_asr|openai|soniox`
  - `realtime_model`
  - `resume_recording_id`
  - `translation_language` (Soniox)
  - `stt_context` JSON encoded (Soniox)
- Server env cho Soniox:
  - `SONIOX_API_KEY`
  - `SONIOX_WS_URL=wss://stt-rt.soniox.com/transcribe-websocket`
  - `SONIOX_MODEL=stt-rt-preview`
  - `SONIOX_LANGUAGE_HINTS=en,vi,ja`
  - `LIVE_TRANSCRIPT_FLUSH_SECONDS=10`
- Backend internal API cho recovery:
  - `POST /api/v1/live-recordings/internal/finalize-interrupted`
  - chỉ service `server` gọi bằng `X-MeetAI-Server-Key`
  - đóng các recording live còn dang dở do server restart, lưu `auto_finalized_reason`, enqueue `recording_sync`

## Test plan
- Transcript source test:
  - API transcript đọc đúng từ `recording_transcripts`
  - `sequence` giữ đúng thứ tự hiển thị
  - `is_important` lưu và đọc đúng
- Permission test:
  - owner/admin/member/share-view mỗi loại chỉ thấy đúng hành vi
  - viewer không thể start/stop/send audio
- Project test:
  - member thấy project ở dashboard
  - soft delete ẩn khỏi list nhưng còn log/audit
- Document test:
  - upload nhiều loại file qua tab `Tài liệu`
  - text-based file đọc trực tiếp đúng nội dung
  - PDF/DOCX/PPTX/XLSX parse bằng Docling và sinh `.extracted.md`
  - `ingestion_status` chuyển đúng `pending -> processing -> done/error`
  - `vector_status` chuyển đúng `queued -> processing -> synced/lexical/error`
  - upload rồi xóa tài liệu trước 10 giây phải cancel document job cũ và không parse file đã xóa
  - sửa mô tả tài liệu phải enqueue lại sync và nội dung mô tả xuất hiện trong knowledge chunk
  - tài liệu xuất hiện trong `project_knowledge_chunks` với `source_type=project_document`
  - nếu model server embedding chạy đúng, `project_knowledge_syncs.retrieval_backend=chroma`
  - dùng `tools/inspect_chroma.py --project-id <project_id>` kiểm tra collection count/sample
  - delete document gỡ khỏi Chroma sau khi sync project
- Contribution test:
  - comment thread hoạt động
  - approve thì index, reject thì không index
- Meeting test:
  - recorder chuyển page vẫn giữ recording
  - tạo meeting từ modal mở recorder global, auto-start đúng và không dừng khi chuyển route trong app
  - Soniox provider không lộ API key ở frontend và không còn cần `NEXT_PUBLIC_SONIOX_API_KEY`
  - frontend WebSocket dùng `NEXT_PUBLIC_WS_URL` để nối tới service `server`
  - Soniox server-side trả được partial/final transcript và translation qua cùng WebSocket
  - Soniox translation dùng model/config hiện hành `stt-rt-v4` + `audio_format=pcm_s16le` + `enable_language_identification`; recording cũ đã completed trước khi lưu translation sẽ không có `translation_text` nếu không chạy backfill
  - nếu Soniox gửi translation token muộn sau khi segment gốc đã commit, service `server` gắn translation vào segment pending/cuối cùng và gọi backend cập nhật `recording_transcripts.translation_text`
  - backend có fallback dịch live transcript trước khi append MongoDB: nếu recording bật `enable_translation` mà segment chưa có `translation_text`, backend dùng LLM dịch theo `translation_language` rồi lưu vào `recording_transcripts`
  - final transcript được lưu MongoDB theo batch 10 giây và flush ngay khi stop/disconnect/finalize
  - khi service `server` bị kill/restart trong lúc họp, backend recovery đóng meeting còn dang dở, enqueue Chroma sync và frontend hiển thị cảnh báo mất tối đa khoảng 10 giây cuối
  - refresh trình duyệt trong lúc recording đang `uploading` sẽ tự reconnect WebSocket bằng `resume_recording_id`; mất mạng không mark done ngay trong thời gian `LIVE_RECONNECT_GRACE_SECONDS=600`, sau 10 phút không reconnect thì recording chuyển `done`
  - khi vào route `/recordings/:id` lần đầu sau auto-restore, client route/context không dùng `async/await` trực tiếp trong Client Component để tránh lỗi hydrate `async/await is not yet supported in Client Components`
  - khi recording chuyển `done`, transcript gốc được lưu vào `project_knowledge_chunks`/Chroma bằng job `recording_sync`, không lưu translation vào chunk
  - `chunk_count` và `duration_seconds` tăng đúng để phục vụ thống kê/trừ credit
  - popup global hoạt động khi người dùng thu nhỏ workspace hoặc chuyển sang khu vực khác trong app
  - trang recording có layout 3 cột: trái hiển thị summary + transcript quan trọng, giữa là transcript realtime, phải là danh sách speaker cho phép đặt tên
  - tên cuộc họp và tên speaker hỗ trợ sửa inline; speaker list mặc định chỉ hiển thị tên + icon edit, không để input mở sẵn
  - speaker list dùng cùng palette màu với speaker chip trong transcript; click item quan trọng sẽ scroll tới transcript tương ứng
  - nút setting recording chỉ hiển thị khi meeting đang live, không hiển thị ở cuộc họp đã kết thúc
  - meeting summary tự sinh lại mỗi 1 phút khi đang live và có transcript đã flush vào MongoDB
  - speaker alias lưu trong `recordings.speaker_labels`; transcript gốc vẫn giữ speaker raw để tránh làm hỏng dữ liệu diarization
  - cài đặt recording cho phép đổi audio driver, nguồn âm thanh và bật/tắt live translation để dùng khi reconnect/khởi động lại stream
  - viewer thấy transcript live qua websocket
  - summary 10 phút cập nhật đúng
- AI test:
  - project chatbot trả lời từ document + transcript + approved contributions
  - chatbot dùng LLM làm câu trả lời cuối, không trả raw top chunk khi đã cấu hình LLM
  - câu chào/hỏi “bạn là ai” không retrieval và không show nguồn
  - câu hỏi nội dung phải retrieval trước khi kết luận không biết
  - câu hỏi đa ngôn ngữ phải tạo query variants và retrieval đúng chunk nguồn dù user hỏi khác ngôn ngữ tài liệu
  - follow-up có history như “Giảm bao nhiêu %?” phải rewrite được về câu hỏi độc lập và lấy đúng dữ liệu trong tài liệu trước đó
  - history dài không được gửi lặp vào rerank/final answer; chỉ rewrite nhìn thấy compact history
  - model purpose routing phải ưu tiên `PROJECT_CHAT_REWRITE_MODEL` cho rewrite, `PROJECT_CHAT_RERANK_MODEL` cho rerank và `PROJECT_CHAT_MODEL`/`LLM_MODEL_NAME` cho final answer
  - rerank phải skip khi số candidate ít/không cần thiết và log rõ `rerank skipped`
  - merge Chroma/lexical không được so raw score trực tiếp; dùng rank fusion để chunk lexical nhiễu từ metadata không đè chunk semantic đúng
  - project document đã upload không bị LLM phủ nhận là “không liên quan dự án” chỉ vì project description khác chủ đề
  - model chat chính 404 thì thử fallback model theo `PROJECT_CHAT_FALLBACK_MODELS`
  - golden questions bắt buộc trước khi coi RAG ổn:
    - `tin_tuc.txt`: “báo cáo tài chính Quý 3” phải lấy được thông tin lợi nhuận giảm 70%
    - `tin_tuc.txt`: follow-up “Giảm bao nhiêu %?” sau câu trên phải trả lời 70%
    - `Manual.pdf`: “Hướng dẫn tôi đăng nhập” phải lấy được quy trình nhập URL, Enter, User ID, Password, Login
    - `Manual.pdf`: “làm sao để thêm khách hàng?” phải tìm đúng phần khách hàng/来店情報 nếu tài liệu có hướng dẫn tương ứng; nếu không có thì nói thiếu dữ liệu dựa trên context đã retrieve, không bịa
  - test retrieval riêng phải assert source/chunk đúng trước khi test câu trả lời LLM; nếu retrieval sai thì không sửa bằng prompt trả lời
  - nút `Trả lời` sinh 3 gợi ý
  - live buffer được dùng trước khi vector flush
- Thiết kế test:
  - tạo `ba_summary` từ nguồn tài liệu/cuộc họp được chọn và các đóng góp đã duyệt, không phụ thuộc transcript của meeting hiện tại
  - `ba_summary` phải bắt đầu trực tiếp bằng plain text section theo ngôn ngữ giao diện, không có câu mở đầu kiểu “Dưới đây là...”, không Markdown heading
  - `stitch_prompt` nội bộ phải bắt đầu bằng `Template goal:` và giữ nguyên screen inventory/ràng buộc từ `ba_summary`
  - source có nhiều module phải sinh danh sách screen đủ rộng; test mẫu HRM/time-card nên có khoảng 8-16 màn hình chính thay vì chỉ 3-4 page
  - generate sau khi user chỉnh `ba_summary`
  - ảnh mẫu thiết kế chỉ được gửi sang Google Stitch, không tạo document/Chroma item
- Mobile test:
  - dashboard, project detail, meeting, chatbot drawer hoạt động tốt ở mobile width

## Giả định đã chốt
- MongoDB tiếp tục là source of truth; không thêm RDBMS.
- Member được chọn từ user đã đăng ký qua search endpoint.
- Mọi entity chính dùng soft delete; vector index phải được remove tương ứng.
- Share link chỉ cho read-only live view của meeting, không mở rộng sang tài liệu hay toàn project.
- `recording_transcripts` là nguồn transcript duy nhất; không giữ lại code liên quan đến field `transcript` trong `recordings`.
- Chỉ transcript final mới được đưa vào vector DB; partial transcript chỉ phục vụ UI/live buffer.
- Flow dựng website demo v1 đã bị gỡ; thiết kế dùng Google Stitch và lưu vào `project_designs`.
- `MODEL_SERVER_BASE_URL` là URL của service `server` (mặc định `http://localhost:8002`), không phải backend.
- `PROJECT_CHROMA_DIR` mặc định ở backend là `tmp/chroma` vì backend thường chạy với working directory `backend/`; nếu dùng `backend/tmp/chroma` khi chạy từ thư mục `backend`, dữ liệu sẽ bị ghi thành `backend/backend/tmp/chroma`.
