From 2ebfbd23d2728971145e30115feb9a7663fba1bc Mon Sep 17 00:00:00 2001 From: MobilBear Date: Tue, 6 Jan 2026 12:30:07 +0800 Subject: [PATCH] Add multiagent inventory mock page and design notes --- multiagent_inventory/README.md | 217 ++++++++++++++ multiagent_inventory/index.html | 206 +++++++++++++ multiagent_inventory/script.js | 380 ++++++++++++++++++++++++ multiagent_inventory/style.css | 497 ++++++++++++++++++++++++++++++++ 4 files changed, 1300 insertions(+) create mode 100644 multiagent_inventory/README.md create mode 100644 multiagent_inventory/index.html create mode 100644 multiagent_inventory/script.js create mode 100644 multiagent_inventory/style.css diff --git a/multiagent_inventory/README.md b/multiagent_inventory/README.md new file mode 100644 index 000000000000..b3fe8797460e --- /dev/null +++ b/multiagent_inventory/README.md @@ -0,0 +1,217 @@ +# Multiagent Feature Identification - Inventory (RMP) + +This folder contains a static mock page plus implementation notes for adding the **Multiagent Feature Identification - Inventory** experience to the Risk Management Platform (RMP). The mock mirrors the existing RMP layout (red header, left sidebar, breadcrumb, filter bar, table, pagination, modals) and is ready to be wired to live APIs. + +## Files +- `index.html` – static mock page with filter bar, paginated table, download actions, and feedback modal. +- `style.css` – styles matching the current RMP look-and-feel (red header, sidebar, card/table/pagination). +- `script.js` – vanilla JS with static data, filtering, sorting, pagination, and feedback submission demo. + +Open the mock locally with any static server, for example: + +```bash +cd multiagent_inventory +python -m http.server 8000 +# then visit http://localhost:8000 +``` + +--- + +## UI Component Breakdown (match existing RMP patterns) + +- **Layout shell**: fixed red top header, left sidebar, main content with breadcrumb + page title. Sidebar adds a new menu entry under **Risk Application → Multiagent Inventory**. +- **Breadcrumb**: `Risk Application / Multiagent / Feature Inventory`. +- **Page title**: `Multiagent Feature Identification - Inventory` with optional badge (e.g., Preview/Beta). +- **Top-right actions**: `Download` (export filtered list), `New` (kept disabled/feature-gated by default). +- **Filter bar** (card header style): + 1. Project ID (text, fuzzy) + 2. Project Name (text, fuzzy) + 3. Current Project Stage (select; dictionary-driven) + 4. Note Generation Date (from/to date pickers) + 5. Run Status (select: Draft/Generated/Failed/Archived) + - Buttons: **Search** (primary red), **Reset** (secondary) +- **Inventory table**: + 1. Project ID (sortable) + 2. Project Name (clickable → detail drawer/page) + 3. Current Project Stage (sortable) + 4. Note Generation Date (sortable) + 5. Run Version (V1/V2/V3) + 6. Generated By + 7. Project Document (Download) + 8. PD Review Report (Download) + 9. Feedback (Add, View(count)) + 10. Actions (View Detail / Archive as needed) +- **Pagination**: default 50 rows/page; shows total and page jump input consistent with RMP. +- **Feedback modal**: prefilled readonly project + user fields; required team dropdown and comments; optional feedback type; Cancel/Submit with toast and refresh. +- **Permission cues**: + - `Multiagent_Admin`: full access, archive, view feedback + - `Multiagent_Reviewer`: can submit feedback and download files + - `Multiagent_ViewOnly`: hide/disable feedback submit and archive; view/download only + - Hide page/menu or show “No Access” when unauthorized. + +--- + +## Backend Controller/Service Pseudocode + +### List Runs +```pseudo +GET /api/multiagent/runs + authz: require role in {Admin, Reviewer, ViewOnly} + params: project_id?, project_name?, stage?, status?, note_generated_from?, note_generated_to?, page=1, page_size=50, sort_field?, sort_dir? + sql: SELECT ... FROM MA_RUN r + LEFT JOIN MA_FILE f_in ON r.input_doc_file_id = f_in.file_id + LEFT JOIN MA_FILE f_out ON r.output_report_file_id = f_out.file_id + WHERE matches(filters) AND not_deleted + ORDER BY sort_field sort_dir + LIMIT page_size OFFSET (page-1)*page_size + result: list + total_count +``` + +### Download File +```pseudo +GET /api/multiagent/files/{file_id}/download + authz: same roles; ensure caller can access project/run + lookup file metadata in MA_FILE; audit log + return streamed file or pre-signed URL from object storage +``` + +### Submit Feedback +```pseudo +POST /api/multiagent/runs/{run_id}/feedback { team_code, comment_text, feedback_type? } + authz: Admin or Reviewer (ViewOnly => 403) + validate: comment length 10-2000, team_code in dictionary + derive identity from session/token (do not trust body) + insert into MA_FEEDBACK (run_id, project_id, user_name, user_email, team_code, comment_text, feedback_type, created_at) + update aggregated feedback_count in MA_RUN (optional optimization) + audit log submission + return created feedback_id +``` + +### Optional Endpoints +- `GET /api/multiagent/runs/{run_id}/feedback` – list feedback for a run. +- `POST /api/multiagent/runs/{run_id}/archive` – Admin only; mark run_status = Archived and hide from default view. + +Service considerations: dictionary cache for stage/team values, consistent error model, and S3/MinIO abstraction for object storage. + +--- + +## SQL Table DDL (minimum) +```sql +CREATE TABLE MA_FILE ( + file_id UUID PRIMARY KEY, + file_type VARCHAR(40) NOT NULL, -- InputDoc, OutputReport, Other + file_name_original TEXT NOT NULL, + storage_key TEXT NOT NULL, -- bucket/key or path + mime_type VARCHAR(120), + file_size BIGINT, + checksum_sha256 CHAR(64), + uploaded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + uploaded_by VARCHAR(120) +); + +CREATE TABLE MA_RUN ( + run_id UUID PRIMARY KEY, + project_id VARCHAR(64) NOT NULL, + run_seq INT NOT NULL, + run_version_label VARCHAR(20) GENERATED ALWAYS AS (CONCAT('V', run_seq)) STORED, + run_status VARCHAR(32) NOT NULL, + note_generated_at TIMESTAMP, + triggered_by_user_email VARCHAR(120), + model_version VARCHAR(64), + input_doc_file_id UUID REFERENCES MA_FILE(file_id), + output_report_file_id UUID REFERENCES MA_FILE(file_id), + feedback_count INT DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(project_id, run_seq) +); + +CREATE TABLE MA_FEEDBACK ( + feedback_id UUID PRIMARY KEY, + run_id UUID NOT NULL REFERENCES MA_RUN(run_id), + project_id VARCHAR(64) NOT NULL, + user_name VARCHAR(120) NOT NULL, + user_email VARCHAR(160) NOT NULL, + user_team_code VARCHAR(40) NOT NULL, + comment_text TEXT NOT NULL, + feedback_type VARCHAR(40), + status VARCHAR(32) DEFAULT 'Open', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP, + resolved_by VARCHAR(160) +); + +-- Suggested indexes +CREATE INDEX idx_ma_run_project ON MA_RUN(project_id, run_status, note_generated_at DESC); +CREATE INDEX idx_ma_feedback_run ON MA_FEEDBACK(run_id, created_at DESC); +``` + +--- + +## Example API Responses + +**List runs** – `GET /api/multiagent/runs?page=1&page_size=2&status=Generated` +```json +{ + "page": 1, + "page_size": 2, + "total": 5, + "items": [ + { + "run_id": "run-1002", + "project_id": "PRJ-1001", + "project_name": "Green Infra Modernization", + "stage": "Monitoring", + "run_seq": 2, + "run_version_label": "V2", + "run_status": "Generated", + "note_generated_at": "2024-10-04T00:00:00Z", + "triggered_by_user_email": "melissa.chen@aiib.org", + "input_doc": { "file_id": "file-in-1002", "file_name": "Project_PRJ-1001_V2_InputDoc_20241004.docx" }, + "output_report": { "file_id": "file-out-1002", "file_name": "Project_PRJ-1001_V2_PDReviewReport_20241004.pdf" }, + "feedback_count": 1 + }, + { + "run_id": "run-1001", + "project_id": "PRJ-1001", + "project_name": "Green Infra Modernization", + "stage": "Due Diligence", + "run_seq": 1, + "run_version_label": "V1", + "run_status": "Generated", + "note_generated_at": "2024-07-18T00:00:00Z", + "triggered_by_user_email": "melissa.chen@aiib.org", + "input_doc": { "file_id": "file-in-1001", "file_name": "Project_PRJ-1001_V1_InputDoc_20240718.pdf" }, + "output_report": { "file_id": "file-out-1001", "file_name": "Project_PRJ-1001_V1_PDReviewReport_20240718.pdf" }, + "feedback_count": 2 + } + ] +} +``` + +**Submit feedback** – `POST /api/multiagent/runs/{run_id}/feedback` +```json +{ + "feedback_id": "fb-24871c3b-3df4-4c58-8f05-6bc3e5a4c845", + "run_id": "run-1002", + "project_id": "PRJ-1001", + "user_name": "Natalie Reviewer", + "user_email": "natalie.reviewer@aiib.org", + "user_team_code": "RM", + "feedback_type": "Enhancement", + "comment_text": "Please include borrower concentration analysis in the next version.", + "created_at": "2025-02-06T12:04:31Z" +} +``` + +--- + +## Integration Notes +- Keep the **New** button feature-gated or disabled until run-creation flow is ready. +- Respect permission model for menu visibility and feedback submission enablement. +- Derive user identity from the session/token server-side; never trust client-sent name/email. +- File downloads should stream or provide pre-signed URLs; suggested naming: + - `Project_{ProjectID}_{RunVersion}_InputDoc_{YYYYMMDD}.pdf` + - `Project_{ProjectID}_{RunVersion}_PDReviewReport_{YYYYMMDD}.pdf` +- Consider adding audit logs for downloads and feedback submissions. + diff --git a/multiagent_inventory/index.html b/multiagent_inventory/index.html new file mode 100644 index 000000000000..e497d58a3963 --- /dev/null +++ b/multiagent_inventory/index.html @@ -0,0 +1,206 @@ + + + + + + Multiagent Feature Identification - Inventory + + + +
+
+ +
+
Risk Management
+
Platform
+
+
+
+
⚙️
+
🔔
+
👤
+
+
+ +
+ + +
+
+ +
+
Multiagent Feature Identification - Inventory Preview
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + +
Project IDProject NameCurrent Project StageNote Generation DateRun VersionGenerated ByProject DocumentPD Review ReportFeedbackActions
+ +
+ + +
+ +

* This static mock demonstrates layout and interaction patterns matching the existing RMP table/filter style. Integrate with live APIs for production.

+
+
+
+ + + +
Feedback submitted
+ + + + diff --git a/multiagent_inventory/script.js b/multiagent_inventory/script.js new file mode 100644 index 000000000000..11ab8cb28588 --- /dev/null +++ b/multiagent_inventory/script.js @@ -0,0 +1,380 @@ +const runs = [ + { + runId: "run-1001", + projectId: "PRJ-1001", + projectName: "Green Infra Modernization", + stage: "Due Diligence", + noteGeneratedAt: "2024-07-18", + runSeq: 1, + runVersionLabel: "V1", + generatedBy: "melissa.chen@aiib.org", + status: "Generated", + inputDoc: { fileId: "file-in-1001", name: "Project_PRJ-1001_V1_InputDoc_20240718.pdf" }, + outputReport: { fileId: "file-out-1001", name: "Project_PRJ-1001_V1_PDReviewReport_20240718.pdf" }, + feedbackCount: 2, + }, + { + runId: "run-1002", + projectId: "PRJ-1001", + projectName: "Green Infra Modernization", + stage: "Monitoring", + noteGeneratedAt: "2024-10-04", + runSeq: 2, + runVersionLabel: "V2", + generatedBy: "melissa.chen@aiib.org", + status: "Generated", + inputDoc: { fileId: "file-in-1002", name: "Project_PRJ-1001_V2_InputDoc_20241004.docx" }, + outputReport: { fileId: "file-out-1002", name: "Project_PRJ-1001_V2_PDReviewReport_20241004.pdf" }, + feedbackCount: 1, + }, + { + runId: "run-2001", + projectId: "PRJ-1044", + projectName: "Digital Inclusion Program", + stage: "Assessment", + noteGeneratedAt: "2024-11-22", + runSeq: 1, + runVersionLabel: "V1", + generatedBy: "liang.hao@aiib.org", + status: "Failed", + inputDoc: { fileId: "file-in-2001", name: "Project_PRJ-1044_V1_InputDoc_20241122.pdf" }, + outputReport: null, + feedbackCount: 0, + }, + { + runId: "run-3001", + projectId: "PRJ-2099", + projectName: "Seismic Resilience Upgrade", + stage: "Approval", + noteGeneratedAt: "2025-01-15", + runSeq: 1, + runVersionLabel: "V1", + generatedBy: "amir.khan@aiib.org", + status: "Draft", + inputDoc: { fileId: "file-in-3001", name: "Project_PRJ-2099_V1_InputDoc_20250115.docx" }, + outputReport: null, + feedbackCount: 0, + }, + { + runId: "run-4001", + projectId: "PRJ-4000", + projectName: "Climate Risk Analytics", + stage: "Monitoring", + noteGeneratedAt: "2024-09-30", + runSeq: 3, + runVersionLabel: "V3", + generatedBy: "wei.zhang@aiib.org", + status: "Archived", + inputDoc: { fileId: "file-in-4001", name: "Project_PRJ-4000_V3_InputDoc_20240930.pdf" }, + outputReport: { fileId: "file-out-4001", name: "Project_PRJ-4000_V3_PDReviewReport_20240930.pdf" }, + feedbackCount: 3, + }, +]; + +const currentUser = { + name: "Natalie Reviewer", + email: "natalie.reviewer@aiib.org", + role: "Multiagent_Reviewer", // Multiagent_Admin | Multiagent_Reviewer | Multiagent_ViewOnly +}; + +const feedbackStore = {}; + +const tableBody = document.querySelector("#inventoryTable tbody"); +const emptyState = document.querySelector("#emptyState"); +const pageInfo = document.querySelector("#pageInfo"); +const pageInput = document.querySelector("#pageInput"); +const pageTotalLabel = document.querySelector("#pageTotal"); +const prevPageBtn = document.querySelector("#prevPage"); +const nextPageBtn = document.querySelector("#nextPage"); +const toast = document.querySelector("#toast"); +const feedbackModal = document.querySelector("#feedbackModal"); +const fbFields = { + projectId: document.getElementById("fbProjectId"), + projectName: document.getElementById("fbProjectName"), + runVersion: document.getElementById("fbRunVersion"), + userName: document.getElementById("fbUserName"), + userEmail: document.getElementById("fbUserEmail"), + team: document.getElementById("fbTeam"), + type: document.getElementById("fbType"), + comment: document.getElementById("fbComment"), +}; +let currentRunId = null; +let sortState = { field: "noteGeneratedAt", direction: "desc" }; +let filtered = [...runs]; +let pageSize = 50; +let currentPage = 1; + +function formatDate(dateStr) { + return new Date(dateStr).toLocaleDateString("en-CA"); +} + +function statusClass(status) { + switch (status) { + case "Generated": + return "status-generated"; + case "Failed": + return "status-failed"; + case "Archived": + return "status-archived"; + default: + return "status-draft"; + } +} + +function applyFilters() { + const projectId = document.getElementById("projectIdInput").value.toLowerCase(); + const projectName = document.getElementById("projectNameInput").value.toLowerCase(); + const stage = document.getElementById("stageSelect").value; + const status = document.getElementById("statusSelect").value; + const dateFrom = document.getElementById("dateFrom").value; + const dateTo = document.getElementById("dateTo").value; + + filtered = runs.filter((run) => { + const matchesId = !projectId || run.projectId.toLowerCase().includes(projectId); + const matchesName = !projectName || run.projectName.toLowerCase().includes(projectName); + const matchesStage = !stage || run.stage === stage; + const matchesStatus = !status || run.status === status; + const matchesDateFrom = !dateFrom || new Date(run.noteGeneratedAt) >= new Date(dateFrom); + const matchesDateTo = !dateTo || new Date(run.noteGeneratedAt) <= new Date(dateTo); + return matchesId && matchesName && matchesStage && matchesStatus && matchesDateFrom && matchesDateTo; + }); + + sortData(); + currentPage = 1; + renderTable(); +} + +function resetFilters() { + document.getElementById("projectIdInput").value = ""; + document.getElementById("projectNameInput").value = ""; + document.getElementById("stageSelect").value = ""; + document.getElementById("statusSelect").value = ""; + document.getElementById("dateFrom").value = ""; + document.getElementById("dateTo").value = ""; + applyFilters(); +} + +function sortData() { + filtered.sort((a, b) => { + const { field, direction } = sortState; + const dir = direction === "asc" ? 1 : -1; + if (field === "noteGeneratedAt") { + return (new Date(a.noteGeneratedAt) - new Date(b.noteGeneratedAt)) * dir; + } + if (field === "projectId" || field === "projectName" || field === "stage") { + return a[field].localeCompare(b[field]) * dir; + } + return 0; + }); +} + +function renderTable() { + tableBody.innerHTML = ""; + if (!filtered.length) { + emptyState.style.display = "block"; + pageInfo.textContent = "Showing 0 of 0"; + pageTotalLabel.textContent = "/ 1"; + return; + } + emptyState.style.display = "none"; + + const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); + currentPage = Math.min(currentPage, totalPages); + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + const pageData = filtered.slice(start, end); + + pageInfo.textContent = `Showing ${start + 1}-${start + pageData.length} of ${filtered.length}`; + pageTotalLabel.textContent = `/ ${totalPages}`; + pageInput.value = currentPage; + + pageData.forEach((run) => { + const tr = document.createElement("tr"); + tr.innerHTML = ` + ${run.projectId} + ${run.projectName} + ${run.stage} + ${formatDate(run.noteGeneratedAt)} + ${run.runVersionLabel} + ${run.generatedBy || "-"} + + ${run.outputReport ? `` : 'Pending'} + ${renderFeedbackCell(run)} + + `; + tableBody.appendChild(tr); + + tr.querySelectorAll("[data-download]").forEach((btn) => + btn.addEventListener("click", () => handleDownload(btn.dataset.download, run)) + ); + + const link = tr.querySelector("[data-detail]"); + link.addEventListener("click", () => handleDetail(run)); + + const fbBtn = tr.querySelector("[data-feedback]"); + fbBtn?.addEventListener("click", () => openFeedback(run)); + + const viewBtn = tr.querySelector("[data-view]"); + viewBtn.addEventListener("click", () => handleDetail(run)); + }); + + prevPageBtn.disabled = currentPage === 1; + nextPageBtn.disabled = currentPage === totalPages; +} + +function renderFeedbackCell(run) { + const count = feedbackStore[run.runId]?.length || run.feedbackCount || 0; + const disabled = currentUser.role === "Multiagent_ViewOnly"; + const viewLabel = count ? `View (${count})` : "View (0)"; + const addBtn = ``; + const viewLink = `${viewLabel}`; + return `${addBtn}
${viewLink}`; +} + +function handleDownload(kind, run) { + if (kind === "input" && run.inputDoc) { + alert(`Download input doc: ${run.inputDoc.name}`); + } + if (kind === "report" && run.outputReport) { + alert(`Download PD Review report: ${run.outputReport.name}`); + } +} + +function handleDetail(run) { + alert(`Open run detail for ${run.projectName} (${run.runVersionLabel})`); +} + +function openFeedback(run) { + if (currentUser.role === "Multiagent_ViewOnly") { + alert("You do not have permission to submit feedback."); + return; + } + currentRunId = run.runId; + fbFields.projectId.value = run.projectId; + fbFields.projectName.value = run.projectName; + fbFields.runVersion.value = run.runVersionLabel; + fbFields.userName.value = currentUser.name; + fbFields.userEmail.value = currentUser.email; + fbFields.team.value = ""; + fbFields.type.value = ""; + fbFields.comment.value = ""; + feedbackModal.style.display = "flex"; +} + +function closeModal() { + feedbackModal.style.display = "none"; +} + +function showToast(message) { + toast.textContent = message; + toast.style.display = "block"; + setTimeout(() => (toast.style.display = "none"), 2000); +} + +function submitFeedback() { + const comment = fbFields.comment.value.trim(); + if (!fbFields.team.value) { + alert("Team is required"); + return; + } + if (comment.length < 10 || comment.length > 2000) { + alert("Comment must be between 10 and 2000 characters."); + return; + } + const feedback = { + feedbackId: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(), + runId: currentRunId, + projectId: fbFields.projectId.value, + userName: currentUser.name, + userEmail: currentUser.email, + teamCode: fbFields.team.value, + commentText: comment, + feedbackType: fbFields.type.value || null, + createdAt: new Date().toISOString(), + }; + feedbackStore[currentRunId] = feedbackStore[currentRunId] || []; + feedbackStore[currentRunId].push(feedback); + showToast("Feedback submitted"); + closeModal(); + renderTable(); +} + +function handleViewFeedback(runId) { + const items = feedbackStore[runId] || []; + if (!items.length) { + alert("No feedback yet for this run."); + return; + } + const summary = items + .map((f) => `• ${f.teamCode} (${f.userName}) @ ${new Date(f.createdAt).toLocaleString()}:\n ${f.commentText}`) + .join("\n\n"); + alert(summary); +} + +function initSortHandlers() { + document.querySelectorAll("th[data-sort]").forEach((th) => { + th.style.cursor = "pointer"; + th.addEventListener("click", () => { + const field = th.dataset.sort; + if (sortState.field === field) { + sortState.direction = sortState.direction === "asc" ? "desc" : "asc"; + } else { + sortState.field = field; + sortState.direction = "asc"; + } + sortData(); + renderTable(); + }); + }); +} + +function initPagination() { + prevPageBtn.addEventListener("click", () => { + currentPage = Math.max(1, currentPage - 1); + renderTable(); + }); + nextPageBtn.addEventListener("click", () => { + const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); + currentPage = Math.min(totalPages, currentPage + 1); + renderTable(); + }); + pageInput.addEventListener("change", () => { + const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); + const target = Math.max(1, Math.min(totalPages, Number(pageInput.value))); + currentPage = target; + renderTable(); + }); +} + +function initFilters() { + document.getElementById("searchBtn").addEventListener("click", applyFilters); + document.getElementById("resetBtn").addEventListener("click", resetFilters); +} + +function initModal() { + document.getElementById("closeModal").addEventListener("click", closeModal); + document.getElementById("cancelModal").addEventListener("click", closeModal); + document.getElementById("submitFeedback").addEventListener("click", submitFeedback); + feedbackModal.addEventListener("click", (e) => { + if (e.target === feedbackModal) closeModal(); + }); +} + +function initDownload() { + document.getElementById("downloadBtn").addEventListener("click", () => { + alert("Export current filtered list to CSV/Excel (wire to backend)"); + }); +} + +function init() { + initFilters(); + initSortHandlers(); + initPagination(); + initModal(); + initDownload(); + applyFilters(); +} + +window.handleViewFeedback = handleViewFeedback; + +document.addEventListener("DOMContentLoaded", init); diff --git a/multiagent_inventory/style.css b/multiagent_inventory/style.css new file mode 100644 index 000000000000..7f5b45243e0a --- /dev/null +++ b/multiagent_inventory/style.css @@ -0,0 +1,497 @@ +:root { + --brand-red: #8f1f21; + --brand-red-dark: #6f1718; + --brand-rose: #d9bebe; + --bg-soft: #f7f5f5; + --text-primary: #212529; + --text-muted: #6c757d; + --border-color: #e2e6ea; + --shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + --radius: 6px; + --sidebar-width: 260px; + --header-height: 60px; + --page-max-width: 1440px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; + color: var(--text-primary); + background: linear-gradient(135deg, #fafafb 0%, #f1f1f3 100%); + min-height: 100vh; +} + +.app-shell { + display: flex; + min-height: 100vh; +} + +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--header-height); + background: linear-gradient(90deg, var(--brand-red) 0%, var(--brand-red-dark) 100%); + color: #fff; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + z-index: 10; + box-shadow: var(--shadow); +} + +.header .brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + letter-spacing: 0.2px; +} + +.brand .logo { + width: 36px; + height: 36px; + border-radius: 50%; + background: #fff; + color: var(--brand-red); + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 18px; +} + +.header .actions { + display: flex; + align-items: center; + gap: 12px; +} + +.circle-icon { + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; +} + +.sidebar { + position: fixed; + top: var(--header-height); + left: 0; + bottom: 0; + width: var(--sidebar-width); + background: linear-gradient(180deg, #fdf8f8 0%, #f2e3e3 100%); + border-right: 1px solid var(--border-color); + padding: 18px 16px; + overflow-y: auto; +} + +.sidebar h4 { + margin: 12px 0 6px; + font-size: 14px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.nav-list { + list-style: none; + padding: 0; + margin: 0; +} + +.nav-item { + margin: 4px 0; +} + +.nav-link { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: var(--radius); + color: var(--text-primary); + text-decoration: none; + font-weight: 500; +} + +.nav-link .icon { + width: 18px; + text-align: center; + color: var(--brand-red); +} + +.nav-link.active { + background: rgba(143, 31, 33, 0.12); + color: var(--brand-red-dark); +} + +.nav-link:hover { + background: rgba(143, 31, 33, 0.08); +} + +.main { + margin-top: var(--header-height); + margin-left: var(--sidebar-width); + padding: 24px; + flex: 1; + display: flex; + justify-content: center; +} + +.content { + width: 100%; + max-width: var(--page-max-width); +} + +.breadcrumb { + font-size: 13px; + color: var(--text-muted); + margin-bottom: 8px; +} + +.page-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.page-title { + font-size: 22px; + font-weight: 650; + display: flex; + align-items: center; + gap: 8px; +} + +.page-title .badge { + background: rgba(143, 31, 33, 0.12); + color: var(--brand-red-dark); + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; +} + +.page-actions { + display: flex; + gap: 10px; + align-items: center; +} + +.btn { + border: 1px solid transparent; + border-radius: var(--radius); + padding: 8px 14px; + font-size: 14px; + cursor: pointer; + transition: all 0.15s ease; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn-primary { + background: var(--brand-red); + color: #fff; + box-shadow: 0 2px 6px rgba(143, 31, 33, 0.3); +} + +.btn-primary:disabled { + background: #c8a1a1; + cursor: not-allowed; + box-shadow: none; +} + +.btn-secondary { + background: #fff; + color: var(--brand-red-dark); + border-color: var(--brand-red); +} + +.btn-ghost { + background: transparent; + border-color: var(--border-color); + color: var(--text-primary); +} + +.btn:hover:not(:disabled) { + transform: translateY(-1px); +} + +.card { + background: #fff; + border: 1px solid var(--border-color); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 14px 14px 8px; +} + +.filter-bar { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 12px; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 4px; +} + +.filter-group label { + font-size: 12px; + color: var(--text-muted); + font-weight: 600; +} + +.filter-group input, +.filter-group select { + height: 36px; + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 0 10px; + font-size: 14px; +} + +.filter-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 4px; +} + +.table-wrapper { + overflow: auto; +} + +.inventory-table { + width: 100%; + border-collapse: collapse; + margin-top: 6px; +} + +.inventory-table th, +.inventory-table td { + padding: 12px 10px; + border-bottom: 1px solid var(--border-color); + text-align: left; + white-space: nowrap; +} + +.inventory-table th { + font-size: 13px; + color: var(--text-muted); + background: #fafafa; + position: sticky; + top: 0; + z-index: 1; +} + +.inventory-table td .link { + color: var(--brand-red); + cursor: pointer; + text-decoration: none; + font-weight: 600; +} + +.status-pill { + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.status-generated { background: #eef8f1; color: #1f7a3d; } +.status-failed { background: #fff0f0; color: #c5393a; } +.status-archived { background: #f5f5f5; color: #6c757d; } +.status-draft { background: #fdf5e6; color: #a76b00; } + +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 4px 6px; + font-size: 14px; + color: var(--text-muted); +} + +.page-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.page-controls input { + width: 60px; + padding: 6px 8px; + border: 1px solid var(--border-color); + border-radius: var(--radius); +} + +.tag { + display: inline-block; + padding: 2px 8px; + background: rgba(143, 31, 33, 0.08); + color: var(--brand-red-dark); + border-radius: 10px; + font-size: 12px; + font-weight: 600; +} + +.feedback-link { + color: var(--brand-red); + text-decoration: none; + font-weight: 600; + cursor: pointer; +} + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: none; + align-items: center; + justify-content: center; + z-index: 20; +} + +.modal { + background: #fff; + border-radius: 10px; + width: 640px; + max-width: 92vw; + box-shadow: var(--shadow); + border: 1px solid var(--border-color); +} + +.modal header { + padding: 16px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal h3 { + margin: 0; +} + +.modal .close-btn { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + color: var(--text-muted); +} + +.modal .body { + padding: 16px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.modal .field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.modal label { + font-size: 13px; + color: var(--text-muted); + font-weight: 600; +} + +.modal input, +.modal select, +.modal textarea { + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 8px 10px; + font-size: 14px; +} + +.modal textarea { + min-height: 100px; + resize: vertical; + grid-column: span 2; +} + +.modal footer { + padding: 14px 16px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.toast { + position: fixed; + right: 20px; + top: calc(var(--header-height) + 16px); + padding: 12px 16px; + background: #1f7a3d; + color: #fff; + border-radius: var(--radius); + box-shadow: var(--shadow); + display: none; + z-index: 25; +} + +.empty-state { + padding: 32px; + text-align: center; + color: var(--text-muted); +} + +.badge-pill { + padding: 2px 10px; + background: rgba(143, 31, 33, 0.12); + border-radius: 12px; + font-size: 12px; + font-weight: 600; +} + +.note { + font-size: 12px; + color: var(--text-muted); +} + +@media (max-width: 1080px) { + .filter-bar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 840px) { + .app-shell { + flex-direction: column; + } + .sidebar { + position: relative; + width: 100%; + height: auto; + display: none; + } + .main { + margin-left: 0; + } +}