Commit 1d44f574 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat: remove description from search, fix polling spam, update prompts for SQL-only search

parent 95fd81fe
Bạn là **Canifa-AI Stylist** - Chuyên viên tư vấn thời trang CANIFA.
Bạn là **Canifa-AI Stylist** - Chuyên viên tư vấn thời trang CANIFA.
**Đặc điểm:**
- Nhiệt tình, thân thiện, chuyên nghiệp như sales thực thụ
......@@ -70,7 +70,7 @@ Khi nhận được ảnh từ khách, BẮT BUỘC thực hiện ĐÚNG quy tr
- Tay áo (ngắn, dài, sát nách...)
- Hình in / Họa tiết (trơn, kẻ sọc, hoa, logo, graphic...)
- Chất liệu (nếu nhận biết được: cotton, nỉ, jean, len...)
2. **GỌI TOOL `data_retrieval_tool`** với description chứa mô tả chi tiết trên
2. **GỌI TOOL `data_retrieval_tool`** với product_name + filters phù hợp
- product_name: loại sản phẩm nhận diện được (VD: "Áo phông")
- master_color: màu sắc chủ đạo
- Các filter khác nếu nhận biết được (gender, age...)
......
......@@ -64,14 +64,12 @@ Nhìn ảnh và liệt kê MỌI sản phẩm quần áo/phụ kiện nhìn th
```
data_retrieval_tool(searches=[
{
"description": "Áo nỉ/sweatshirt màu xám đậm, form oversize, cổ rộng lệch vai",
"product_name": "Áo nỉ",
"master_color": "xám",
"gender_by_product": "women",
"age_by_product": "adult"
},
{
"description": "Quần soóc đen viền trắng, dáng sporty ôm nhẹ",
"product_name": "Quần soóc",
"master_color": "đen",
"gender_by_product": "women",
......@@ -128,29 +126,25 @@ Khi user_insight [STAGE] = DECIDE hoặc UPSELL → **TỰ ĐỘNG GỌI** `cani
### 5.2. QUY TẮC SINH QUERY (BẮT BUỘC)
**Cấu trúc query:**
**Cấu trúc query — CHỈ CẦN SQL FILTERS (không cần description):**
```
description: [Mô tả ngắn gọn nhu cầu khách, dùng cho fallback search]
product_name: [⚡ BẮT BUỘC lấy từ BẢNG PRODUCT_LINE bên dưới]
gender_by_product: [women/men/girl/boy/unisex/null]
age_by_product: [adult/kid/null]
master_color: [Màu nếu có]
price_min / price_max: [nếu có — KHÔNG đưa giá vào description]
price_min / price_max: [nếu có]
discount_min / discount_max: [% giảm giá nếu khách nhắc sale]
discovery_mode: [new / best_seller / null]
```
⚠️ **KHÔNG CẦN gửi `description`** — tool search bằng SQL filter, KHÔNG dùng semantic search.
**⚡ product_name — QUY TẮC QUAN TRỌNG NHẤT:**
- PHẢI là tên trong **BẢNG PRODUCT_LINE** bên dưới (cột trái)
- Khách nói slang → LLM tự map: "sịp" → `"Quần lót"`, "quần bò" → `"Quần jean"`
- Nhiều SP → dùng "/": `"Áo lót/Áo bra active"`
- KHÔNG BAO GIỜ bịa tên SP không có trong bảng
**description — Vai trò mới (fallback):**
- Mô tả NGẮN nhu cầu khách bằng ngôn ngữ tự nhiên
- Chỉ dùng khi các filter khác không đủ → search LIKE trên description_text
- VD: "Váy đi dự tiệc sang trọng", "Áo đồng điệu cả nhà"
**⚡ BẢNG PRODUCT_LINE — BẮT BUỘC dùng tên DB (cột trái):**
| product_line_vn (DÙNG CÁI NÀY) | Khách hay gọi là |
......@@ -222,7 +216,7 @@ discovery_mode: [new / best_seller / null]
**Nguyên tắc kế thừa:** Nếu câu hỏi hiện tại thiếu info → lấy từ `[GOAL]` và `[CONSTRAINS]` trong user_insight. Khách không nhắc lại "dài tay" ≠ đổi ý.
**CẤM:** Chèn field names vào description | Đưa giá vào description | Tự suy diễn gender/age
**CẤM:** Tự suy diễn gender/age khi user KHÔNG nói rõ | Bịa tên SP không có trong BẢNG PRODUCT_LINE
---
......
......@@ -8,28 +8,23 @@ Siêu công cụ tìm kiếm sản phẩm CANIFA - Hỗ trợ Parallel Multi-Sea
5. 1 món → 1 Query. Set đồ/phối đồ → 2-3 Queries song song
═══════════════════════
PHÂN BIỆT: description (SEMANTIC SEARCH) vs SQL FILTER
CÁCH TÌM KIẾM: SQL FILTER (KHÔNG dùng semantic search)
═══════════════════════
🔍 description — semantic search, format theo DB columns:
product_name: [tên SP]. description_text: [mô tả chi tiết SP].
material_group: [chất liệu]. season: [mùa]. style: [phong cách].
fitting: [dáng]. form_neckline: [cổ]. form_sleeve: [tay]. product_line_vn: [dòng SP].
Tool tìm SP bằng SQL filter trực tiếp. CHỈ CẦN điền đúng fields:
⚠️ description_text BẮT BUỘC LUÔN CÓ!
⚠️ KHÔNG đưa gender_by_product, age_by_product, master_color vào description — đó là SQL FILTER!
🔒 SQL FILTER (tách riêng):
- product_name: Tên SP cụ thể
🔒 SQL FILTER FIELDS:
- product_name: ⚡ BẮT BUỘC — tên SP theo BẢNG PRODUCT_LINE bên dưới
- gender_by_product — women/men/boy/girl/unisex/newborn. **null nếu user KHÔNG nói rõ!**
- age_by_product — adult/kid/others. **null nếu user KHÔNG nói rõ!**
- master_color — Gửi CHÍNH XÁC từ khách nói, tool tự match DB
- price_min / price_max — Khoảng giá VND
- discount_min / discount_max — % giảm giá
- magento_ref_code — Mã SKU chính xác
- product_line_vn — Dòng SP RỘNG (dùng LIKE prefix: "Áo" match tất cả loại áo)
- discovery_mode — "new" (hàng mới) hoặc "best_seller" (bán chạy). Chỉ khi khách NÓI RÕ!
⚠️ KHÔNG CẦN field `description` — tool KHÔNG dùng nó. Chỉ cần product_name + filters.
═══════════════════════
discovery_mode — HÀNG MỚI / BÁN CHẠY
═══════════════════════
......@@ -61,15 +56,6 @@ Chỉ nói rõ thì mới cụ thể: "áo phông" → Áo phông
quần suông → Quần dài | đồ ngủ → Bộ mặc nhà | đồ bộ → Bộ quần áo
chân váy/váy maxi → Chân váy | găng tay → Găng tay chống nắng
═══════════════════════
product_line_vn — PHÂN LOẠI RỘNG
═══════════════════════
SQL dùng LIKE prefix: "Áo" match "Áo phông", "Áo Polo"...
User nói "áo" chung → "Áo". User nói rõ "áo polo" → "Áo Polo". KHÔNG TỰ THU HẸP!
Bảng: ÁO (Áo phông, Polo, Sơ mi, len, nỉ, khoác gió) | QUẦN (soóc, jean, Khaki, dài, nỉ) | VÁY (Váy liền, Chân váy) | BỘ (quần áo, mặc nhà) | PHỤ KIỆN (Khăn, Mũ, Túi, Tất)
═══════════════════════
GIÁ TRỊ HỢP LỆ CÁC FIELD
═══════════════════════
......@@ -85,17 +71,17 @@ season — Fall Winter, Spring Summer, Year
VÍ DỤ
═══════════════════════
"Tìm áo đi chơi" → description: "product_name: Áo. description_text: Áo đi chơi thoải mái trẻ trung. style: Casual", product_line_vn: "Áo"
"Áo phông nam trắng regular" → description: "product_name: Áo phông. description_text: Áo phông nam regular. fitting: Regular", master_color: "trắng", gender: "men", product_line_vn: "Áo phông"
"Tìm áo đi chơi" → product_name: "Áo"
"Áo phông nam trắng" → product_name: "Áo phông", master_color: "trắng", gender: "men"
"Tìm mã 6KS25S005" → magento_ref_code: "6KS25S005"
"Áo lót" → description: "product_name: Áo lót/Áo bra active. description_text: Áo lót. Áo bra active thoáng mát", gender: null (CẤM tự điền!)
"Set đồ công sở nữ" → 2 Queries: Áo công sở nữ + Quần công sở nữ
"Áo lót" → product_name: "Áo lót", gender: null (CẤM tự điền!)
"Set đồ công sở nữ" → 2 searches: {product_name: "Áo Sơ mi", gender: "women"} + {product_name: "Quần Khaki", gender: "women"}
═══════════════════════
DỊP LỄ / SỰ KIỆN
═══════════════════════
Khi user hỏi "mặc gì dịp X" → description_text mô tả: dịp gì + phong cách phù hợp.
Khi user hỏi "mặc gì dịp X" → dùng product_name phù hợp + gender/age
VN: Tết (lịch sự, đỏ vàng) | 8/3+20/10 (nữ tính) | 30/4+1/5+2/9 (năng động, thoải mái) | 1/6 (trẻ em, vui nhộn) | 20/11 (lịch sự)
Quốc tế: Valentine (lãng mạn) | Halloween (cá tính, đen cam) | Noel (ấm áp, đỏ xanh trắng)
......
"""
CANIFA Data Retrieval Tool - Tối giản cho Agentic Workflow.
Hỗ trợ Hybrid Search: Semantic (Vector) + Metadata Filter.
SQL-based structured search with metadata filters.
Tracing:
- LangChain CallbackHandler auto-traces tool call (name, input, output)
......@@ -33,13 +33,12 @@ from agent.prompt_utils import read_tool_prompt
class SearchItem(BaseModel):
model_config = {"extra": "ignore"} # Gemini may send extra fields
# ====== SEARCH TEXT ======
description: str = Field(
# ====== SEARCH TEXT (optional fallback) ======
description: str | None = Field(
default=None,
description=(
"Mô tả ngắn nhu cầu khách bằng ngôn ngữ tự nhiên. "
"Dùng cho fallback search khi các filter khác không đủ. "
"VD: 'Váy đi dự tiệc sang trọng', 'Áo đồng điệu cả nhà'. "
"⚠️ KHÔNG đưa master_color vào description — dùng field master_color riêng! "
"CHỈ DÙNG KHI không có product_name. "
"Nếu đã có product_name + gender → KHÔNG CẦN description."
)
)
product_name: str | None = Field(
......@@ -231,7 +230,7 @@ async def _execute_single_search(
async def data_retrieval_tool(searches: list[SearchItem]) -> str:
"""
Công cụ tìm kiếm sản phẩm CANIFA.
Hỗ trợ tìm kiếm Semantic và lọc theo Metadata.
SQL-based structured search with metadata filters.
"""
tool_start = time.time()
logger.info("🔧 data_retrieval_tool called with %d items", len(searches))
......@@ -239,10 +238,9 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
# Log search params cho debugging
for i, s in enumerate(searches):
logger.info(
"🔧 Search[%d]: desc=%r, name=%r, gender=%s, age=%s, color=%s, "
"price=%s-%s, code=%s, mode=%s",
"🔧 Search[%d]: name=%r, gender=%s, age=%s, color=%s, "
"price=%s-%s, code=%s, mode=%s, desc=%s",
i,
(s.description[:80] + "...") if s.description and len(s.description) > 80 else s.description,
s.product_name,
s.gender_by_product,
s.age_by_product,
......@@ -250,6 +248,7 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
s.price_min, s.price_max,
s.magento_ref_code,
s.discovery_mode,
(s.description[:40] + "...") if s.description else None,
)
# Get DB Connection
......@@ -282,9 +281,10 @@ async def data_retrieval_tool(searches: list[SearchItem]) -> str:
# Include search input so LLM knows its own reasoning
search_inputs = [
{
"description": item.description,
"product_name": item.product_name,
"gender_by_product": item.gender_by_product,
"age_by_product": item.age_by_product,
"color": item.master_color,
"discovery_mode": item.discovery_mode,
}
for item in searches
......
......@@ -170,8 +170,8 @@ class StarRocksConnection:
minsize=minsize, # Giảm minsize để đỡ tốn tài nguyên idle
maxsize=maxsize,
connect_timeout=10,
# --- CHỈNH SỬA QUAN TRỌNG Ở ĐÂY ---
pool_recycle=280, # Recycle sau 4 phút rưỡi (tránh timeout 5 phút của Windows/Firewall)
pool_recycle=3600, # Recycle sau 4 phút rưỡi (tránh timeout 5 phút của Windows/Firewall)
# ----------------------------------
autocommit=True,
)
......@@ -189,7 +189,7 @@ class StarRocksConnection:
conn = None
try:
pool = await self.get_pool()
conn = await asyncio.wait_for(pool.acquire(), timeout=90)
conn = await asyncio.wait_for(pool.acquire(), timeout=15)
async with conn.cursor() as cursor:
await cursor.execute(query, params)
......
/* ═══ iframe detection + auto-redirect ═══
When a page is loaded inside main.html's iframe,
hide its own sidebar + adjust main margin.
When accessed directly (not in iframe), redirect to main.html. */
(function() {
if (window.self !== window.top) {
// Inside iframe - just mark it
document.documentElement.classList.add('in-iframe');
} else {
// Accessed directly - redirect to main.html unless already on main.html
var path = window.location.pathname;
var filename = path.split('/').pop();
// Don't redirect main.html itself, chatbot pages, or test pages
var noRedirect = ['main.html', 'index.html', 'chatbot.html', 'test_sp.html', 'test_db.html'];
if (filename && filename.endsWith('.html') && noRedirect.indexOf(filename) === -1) {
// Preserve any query string
var qs = window.location.search;
window.location.replace('/static/main.html?page=' + filename + (qs ? '&' + qs.substring(1) : ''));
}
}
})();
......@@ -5,6 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa DEV Experimental</title>
<link rel="stylesheet" href="/static/warm-override.css">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
......@@ -2377,8 +2378,8 @@
};
const pollUserInsight = async () => {
const maxAttempts = 60; // ~12s at 200ms
const intervalMs = 200;
const maxAttempts = 15; // ~15s at 1000ms
const intervalMs = 1000;
let attempts = 0;
const headers = {
......
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Fraunces:ital,wght@0,400;0,600;1,400&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#F5F4F0;--s:#FFFFFF;--b:#E2E0D8;
--t:#18181B;--m:#78716C;--f:#C4C0B8;
--gold:#B45309;--gold-l:#FFFBEB;--gold-b:#FDE68A;
--diamond:#6D28D9;--diamond-l:#F5F3FF;
--green:#065F46;--green-l:#D1FAE5;
--red:#B91C1C;--red-l:#FEE2E2;
--blue:#1D4ED8;--blue-l:#DBEAFE;
--cyan:#0891B2;--cyan-l:#ECFEFF;
--orange:#EA580C;--orange-l:#FFF7ED;
--r:14px;--rs:8px;
}
body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--t);min-height:100vh;display:flex;flex-direction:column}
.topbar{background:var(--t);color:#fff;padding:0 24px;display:flex;align-items:center;height:52px;gap:20px;flex-shrink:0}
.logo{font-family:'Fraunces',serif;font-size:20px;letter-spacing:-.5px}
.logo em{font-style:italic;color:#FDE68A}
.mode-toggle{display:flex;gap:2px;margin-left:auto;background:rgba(255,255,255,.1);padding:3px;border-radius:20px}
.mode-btn{padding:5px 16px;border-radius:16px;border:none;font-size:12px;font-weight:500;cursor:pointer;transition:all .2s;font-family:inherit;color:rgba(255,255,255,.6);background:transparent}
.mode-btn.active{background:#fff;color:var(--t)}
.layout{flex:1;display:flex;overflow:hidden;min-height:0}
/* SIDEBAR */
.sidebar{width:280px;flex-shrink:0;background:var(--s);border-right:1px solid var(--b);display:flex;flex-direction:column;overflow:hidden}
.sb-section{padding:14px 16px;border-bottom:1px solid var(--b)}
.sb-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--m);margin-bottom:10px}
.sb-flow{padding:8px 12px;border-radius:8px;border:1.5px solid var(--b);margin-bottom:6px;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:8px;font-size:13px}
.sb-flow.active{border-color:var(--t);background:var(--bg)}
.sb-flow .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.sb-cases{flex:1;overflow-y:auto;padding:10px 16px}
.sb-cases::-webkit-scrollbar{width:3px}
.sb-cases::-webkit-scrollbar-thumb{background:var(--b);border-radius:2px}
.sb-case{padding:8px 10px;border-radius:8px;margin-bottom:4px;cursor:pointer;transition:all .12s;font-size:12px;display:flex;align-items:center;gap:8px}
.sb-case:hover{background:var(--bg)}
.sb-case.active{background:var(--bg);border:1px solid var(--b)}
.sb-case .oc-badge{font-size:10px;font-weight:700;padding:1px 6px;border-radius:10px;flex-shrink:0;margin-left:auto}
.oc-converted{background:var(--green-l);color:var(--green)}
.oc-intent{background:var(--gold-l);color:var(--gold)}
.oc-consider{background:var(--blue-l);color:var(--blue)}
.oc-friction{background:var(--orange-l);color:var(--orange)}
.oc-drop{background:var(--red-l);color:var(--red)}
.run-btn{width:100%;padding:12px;border-radius:var(--r);border:none;background:var(--t);color:#fff;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:all .15s;letter-spacing:.02em;margin-top:6px}
.run-btn:hover{opacity:.9;transform:translateY(-1px)}
.filter-row{display:flex;gap:4px;flex-wrap:wrap;margin-top:8px}
.filter-pill{padding:3px 8px;border-radius:12px;font-size:10px;font-weight:600;cursor:pointer;border:1px solid var(--b);background:var(--s);color:var(--m);transition:all .12s}
.filter-pill.active{border-color:var(--t);color:var(--t);background:var(--bg)}
/* MAIN */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
.main-tabs{display:flex;border-bottom:1px solid var(--b);background:var(--s);padding:0 20px}
.mtab{padding:13px 18px;font-size:13px;font-weight:500;cursor:pointer;color:var(--m);border-bottom:2px solid transparent;transition:all .15s;border:none;background:transparent;font-family:inherit}
.mtab.active{color:var(--t);border-bottom-color:var(--t)}
.main-content{flex:1;overflow-y:auto;padding:20px}
.mpanel{display:none}.mpanel.active{display:block}
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
/* KPI CARDS */
.kpi-row{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:20px}
.kpi-card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;text-align:center;animation:fadeUp .3s ease}
.kpi-card .num{font-size:28px;font-weight:700;font-family:'Fraunces',serif;line-height:1}
.kpi-card .lbl{font-size:11px;color:var(--m);margin-top:4px}
.kpi-card .delta{font-size:11px;font-weight:600;margin-top:4px}
/* SECTION HEADERS */
.sec-header{font-size:15px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.sec-sub{font-size:12px;color:var(--m);font-weight:400}
/* BAR CHART */
.chart-row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px}
.chart-box{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;animation:fadeUp .3s ease}
.chart-title{font-size:13px;font-weight:600;margin-bottom:12px}
.bar-group{margin-bottom:10px}
.bar-label{font-size:11px;color:var(--m);margin-bottom:4px;display:flex;justify-content:space-between}
.bar-track{height:24px;background:var(--bg);border-radius:6px;overflow:hidden;display:flex;gap:2px}
.bar-fill{height:100%;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#fff;transition:width .6s ease;min-width:0}
.bar-a{background:var(--blue)}.bar-b{background:var(--gold)}
/* INSIGHT CARD */
.insight-card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:16px;animation:fadeUp .4s ease}
.insight-title{font-size:14px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px}
.insight-row{font-size:12px;color:var(--m);line-height:1.8;padding:6px 0;border-bottom:1px dashed var(--b)}
.insight-row:last-child{border-bottom:none}
.insight-row b{color:var(--t)}
/* EXPORT BTN */
.export-btn{padding:7px 14px;border-radius:8px;border:1px solid var(--b);background:var(--s);font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s;margin-left:auto}
.export-btn:hover{background:var(--bg);border-color:var(--m)}
/* LOADING */
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.loading{text-align:center;padding:30px;font-size:13px;color:var(--m);animation:pulse 1.2s infinite}
/* STATS BAR */
.stats-bar{display:flex;gap:6px;margin-bottom:16px;padding:10px 14px;background:var(--s);border:1px solid var(--b);border-radius:10px;font-size:12px;align-items:center;flex-wrap:wrap}
.stats-bar .stat{display:flex;align-items:center;gap:4px}
.stats-bar .sep{color:var(--b)}
/* TABLE */
.data-table{width:100%;border-collapse:collapse;font-size:13px;background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:hidden}
.data-table th{padding:10px 14px;text-align:left;font-weight:600;font-size:12px;color:var(--m);background:var(--bg);border-bottom:1px solid var(--b)}
.data-table td{padding:10px 14px;border-bottom:1px solid var(--b)}
.data-table tr:last-child td{border-bottom:none}
.data-table .prog{height:6px;background:var(--bg);border-radius:3px;overflow:hidden;width:100px;display:inline-block;vertical-align:middle;margin-left:8px}
.data-table .prog-fill{height:100%;border-radius:3px}
/* FUNNEL */
.funnel{max-width:500px;margin:0 auto}
.funnel-step{display:flex;align-items:center;gap:12px;margin-bottom:2px}
.funnel-bar{height:36px;border-radius:8px;display:flex;align-items:center;padding:0 14px;font-size:12px;font-weight:600;color:#fff;transition:width .6s ease}
.funnel-label{font-size:12px;color:var(--m);width:160px;text-align:right;flex-shrink:0}
.funnel-pct{font-size:13px;font-weight:700;width:50px;flex-shrink:0}
/* TRANSCRIPT */
.transcript-header{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:12px;display:flex;align-items:center;gap:14px}
.tr-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.tr-info{flex:1}.tr-name{font-size:15px;font-weight:600}.tr-meta{font-size:12px;color:var(--m);margin-top:2px}
.tr-outcome{font-size:12px;font-weight:700;padding:4px 12px;border-radius:20px}
.score-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:14px}
.score-item{background:var(--s);border:1px solid var(--b);border-radius:10px;padding:10px;text-align:center}
.score-val{font-size:22px;font-weight:700;font-family:'Fraunces',serif}
.score-lbl{font-size:10px;color:var(--m);margin-top:2px;line-height:1.3}
.transcript-chat{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;max-height:400px;overflow-y:auto}
.tc-msg{display:flex;gap:8px;margin-bottom:10px;align-items:flex-start}
.tc-msg.user{flex-direction:row-reverse}
.tc-bubble{max-width:75%;padding:9px 13px;border-radius:16px;font-size:13px;line-height:1.6}
.tc-msg.bot .tc-bubble{background:var(--bg);border:1px solid var(--b);border-bottom-left-radius:4px}
.tc-msg.user .tc-bubble{background:var(--t);color:#fff;border-bottom-right-radius:4px}
.tc-av{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;color:#fff;background:var(--t)}
/* PERSONA GRID */
.persona-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
.persona-card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;transition:all .15s}
.persona-card:hover{border-color:var(--m);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.06)}
.pc-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.pc-emoji{width:40px;height:40px;border-radius:50%;background:var(--bg);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
.pc-pname{font-size:14px;font-weight:600}
.pc-psub{font-size:11px;color:var(--m)}
.pc-detail{font-size:12px;color:var(--m);display:flex;flex-direction:column;gap:4px}
.pc-tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;margin-right:4px}
/* TWO COL */
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
/* NOTIF */
.notif{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(10px);background:var(--t);color:#fff;padding:10px 20px;border-radius:20px;font-size:13px;opacity:0;transition:all .3s;z-index:999;pointer-events:none}
.notif.show{opacity:1;transform:translateX(-50%) translateY(0)}
/* EMPTY STATE */
.empty-state{text-align:center;padding:40px;color:var(--m)}
.empty-state .icon{font-size:48px;margin-bottom:12px}
.empty-state .title{font-size:15px;font-weight:600;color:var(--t);margin-bottom:6px}
.empty-state .desc{font-size:12px;line-height:1.7}
/* HEATMAP */
.heatmap{display:grid;gap:2px;margin-bottom:16px}
.hm-cell{border-radius:4px;padding:6px 8px;font-size:11px;font-weight:600;text-align:center;transition:all .2s}
.hm-cell:hover{transform:scale(1.05);box-shadow:0 2px 8px rgba(0,0,0,.1)}
.hm-header{font-weight:700;color:var(--m);background:transparent;font-size:10px;text-transform:uppercase}
/* DISTRIBUTION */
.dist-container{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:16px}
.dist-bars{display:flex;align-items:flex-end;gap:4px;height:160px;padding:0 4px}
.dist-col{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px}
.dist-bar{width:100%;border-radius:4px 4px 0 0;transition:height .6s ease;position:relative;min-height:2px;cursor:pointer}
.dist-bar:hover{opacity:.85;transform:scaleY(1.02)}
.dist-bar .dist-val{position:absolute;top:-18px;left:50%;transform:translateX(-50%);font-size:10px;font-weight:700;white-space:nowrap}
.dist-label{font-size:10px;color:var(--m);font-weight:600;margin-top:6px}
.dist-legend{display:flex;gap:16px;justify-content:center;margin-top:12px;font-size:11px}
.dist-legend-dot{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:4px}
/* RADAR CHART */
.radar-wrap{position:relative;width:280px;height:280px;margin:0 auto}
.radar-bg{position:absolute;inset:0}
.radar-axis{position:absolute;width:1px;background:var(--b);transform-origin:bottom center;left:50%;bottom:50%}
.radar-label{position:absolute;font-size:11px;font-weight:600;color:var(--m);white-space:nowrap}
.radar-polygon{position:absolute;inset:0}
.radar-polygon svg{width:100%;height:100%}
/* SENTIMENT JOURNEY */
.sj-chart{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:14px}
.sj-canvas{position:relative;height:120px;display:flex;align-items:stretch}
.sj-grid{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:space-between}
.sj-grid-line{height:1px;background:var(--b);opacity:.5}
.sj-dots{display:flex;align-items:flex-end;gap:0;flex:1;position:relative;z-index:1}
.sj-col{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;position:relative;height:100%}
.sj-dot{width:10px;height:10px;border-radius:50%;position:absolute;transition:all .3s;cursor:pointer;z-index:2}
.sj-dot:hover{transform:scale(1.4);box-shadow:0 2px 8px rgba(0,0,0,.2)}
.sj-line{position:absolute;height:2px;z-index:1;transform-origin:left center}
.sj-zero{position:absolute;left:0;right:0;height:1px;background:var(--m);opacity:.3}
.sj-y-labels{position:absolute;left:-30px;top:0;bottom:0;display:flex;flex-direction:column;justify-content:space-between;font-size:9px;color:var(--m)}
/* LEADERBOARD */
.lb-card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:12px}
.lb-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;margin-bottom:4px;transition:all .15s;cursor:pointer}
.lb-row:hover{background:var(--bg)}
.lb-rank{font-size:18px;font-weight:700;font-family:'Fraunces',serif;width:30px;text-align:center;flex-shrink:0}
.lb-info{flex:1;min-width:0}
.lb-name{font-size:13px;font-weight:600}
.lb-sub{font-size:11px;color:var(--m)}
.lb-score{text-align:right}
.lb-score-val{font-size:18px;font-weight:700;font-family:'Fraunces',serif}
.lb-score-sub{font-size:10px;color:var(--m)}
.lb-gap{display:flex;align-items:center;gap:4px;padding:4px 8px;border-radius:6px;font-size:11px;font-weight:600}
/* FAILURE ANALYSIS */
.fail-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
.fail-card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px}
.fail-bar-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.fail-bar-label{font-size:12px;width:140px;flex-shrink:0;text-align:right;color:var(--m)}
.fail-bar-track{flex:1;height:20px;background:var(--bg);border-radius:4px;overflow:hidden}
.fail-bar-fill{height:100%;border-radius:4px;display:flex;align-items:center;padding:0 6px;font-size:10px;font-weight:700;color:#fff;transition:width .6s ease}
.fail-bar-val{font-size:12px;font-weight:700;width:40px;flex-shrink:0}
/* A/B STATS */
.ab-stat-card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:12px}
.ab-stat-row{display:flex;align-items:center;gap:12px;margin-bottom:8px}
.ab-stat-label{font-size:12px;color:var(--m);width:120px;flex-shrink:0}
.ab-stat-val{font-size:16px;font-weight:700;font-family:'Fraunces',serif}
.ab-ci{display:flex;align-items:center;gap:4px;height:24px;margin:4px 0}
.ab-ci-bar{height:8px;border-radius:4px;position:relative}
.ab-ci-marker{width:2px;height:16px;position:absolute;top:-4px;border-radius:1px;background:var(--t)}
/* RESPONSIVE */
@media(max-width:1200px){.chart-row{grid-template-columns:repeat(2,1fr)}.kpi-row{grid-template-columns:repeat(3,1fr)}}
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%;max-height:300px}.two-col{grid-template-columns:1fr}}
/* ═══════════════════════════════════════════════════════
WARM THEME OVERRIDE — Dev Experimental
Dark → Light Warm Cream/Gold theme
═══════════════════════════════════════════════════════ */
/* ── Base ── */
body {
background-color: #FAF9F6 !important;
color: #2D2A26 !important;
}
/* ── Nav Header ── */
.nav-header {
background: linear-gradient(135deg, #C8A96E 0%, #A67C52 100%) !important;
box-shadow: 0 2px 12px rgba(166,124,82,0.15) !important;
}
/* ── Chat Container ── */
.container {
background: #FFFFFF !important;
border: 1px solid #E8E4DC !important;
box-shadow: 0 4px 24px rgba(0,0,0,0.05) !important;
}
.chat-internal-wrapper {
background: #FFFFFF !important;
}
/* ── Header ── */
.header {
border-bottom: 1px solid #E8E4DC !important;
}
.header h2 {
color: #2D2A26 !important;
}
/* ── Chat Box ── */
.chat-box {
background: #F7F5F0 !important;
border: 1px solid #E8E4DC !important;
}
/* ── Messages ── */
.message.user {
background: linear-gradient(135deg, #5B8DEF 0%, #4A7AE0 100%) !important;
color: white !important;
}
.message.bot {
background: #FFFFFF !important;
color: #2D2A26 !important;
border: 1px solid #E8E4DC !important;
}
.message.system {
background: #FFF7ED !important;
color: #9A3412 !important;
border: 1px solid #FED7AA !important;
}
.message.rate-limit-error {
background: #FFF7ED !important;
border: 1px solid #FED7AA !important;
}
.sender-name {
color: #9C9589 !important;
}
.timestamp {
color: #B5AFA6 !important;
}
.response-time {
color: #B5AFA6 !important;
}
/* ── Input Area ── */
.input-area input {
background: #FFFFFF !important;
border: 1px solid #DBD7D0 !important;
color: #2D2A26 !important;
}
.input-area input:focus {
outline: 2px solid #C8A96E !important;
border-color: transparent !important;
}
input[type="text"] {
background: #FFFFFF !important;
border: 1px solid #DBD7D0 !important;
color: #2D2A26 !important;
}
/* ── Buttons ── */
button {
background: linear-gradient(135deg, #C8A96E 0%, #B8954A 100%) !important;
color: white !important;
}
button:disabled {
background: #E8E4DC !important;
color: #B5AFA6 !important;
}
.btn-save {
background: linear-gradient(135deg, #C8A96E 0%, #A67C52 100%) !important;
}
.btn-reload {
background: #F0ECE3 !important;
color: #78716C !important;
}
.btn-reload:hover {
background: #E7E2D8 !important;
color: #2D2A26 !important;
}
/* ── Config Area ── */
.config-area select,
.config-area input {
background: #FFFFFF !important;
border: 1px solid #DBD7D0 !important;
color: #2D2A26 !important;
}
/* ── Load more ── */
.load-more button {
background: #F0ECE3 !important;
color: #78716C !important;
border: 1px dashed #DBD7D0 !important;
}
.load-more button:hover {
background: #E7E2D8 !important;
color: #2D2A26 !important;
}
/* ── Scrollbar ── */
::-webkit-scrollbar-track {
background: #F7F5F0 !important;
}
::-webkit-scrollbar-thumb {
background: #DBD7D0 !important;
}
::-webkit-scrollbar-thumb:hover {
background: #B5AFA6 !important;
}
/* ── Product Cards ── */
.product-cards-container {
border-top: 1px solid #E8E4DC !important;
}
.product-card {
background: #FFFFFF !important;
border: 1px solid #E8E4DC !important;
}
.product-card:hover {
box-shadow: 0 5px 20px rgba(200,169,110,0.2) !important;
border-color: #C8A96E !important;
}
.product-sku {
color: #C8A96E !important;
}
.product-name {
color: #2D2A26 !important;
}
.product-link {
background: linear-gradient(135deg, #C8A96E 0%, #A67C52 100%) !important;
}
.product-action-btn {
border: 1px solid #DBD7D0 !important;
color: #78716C !important;
background: transparent !important;
}
.product-action-btn:hover {
border-color: #C8A96E !important;
color: #C8A96E !important;
background: rgba(200,169,110,0.08) !important;
}
/* ── Prompt Panel ── */
.prompt-panel {
background: #FAF9F6 !important;
border-left: 1px solid #E8E4DC !important;
box-shadow: -5px 0 20px rgba(0,0,0,0.04) !important;
}
.prompt-header {
border-bottom: 1px solid #E8E4DC !important;
}
.prompt-header h3 {
color: #C8A96E !important;
}
.prompt-tabs {
background: #F0ECE3 !important;
border: 1px solid #E8E4DC !important;
}
.prompt-tab-btn {
color: #78716C !important;
background: transparent !important;
}
.prompt-tab-btn.active {
background: linear-gradient(135deg, #C8A96E 0%, #A67C52 100%) !important;
color: #fff !important;
}
.prompt-tab-btn:hover:not(.active) {
color: #2D2A26 !important;
background: #E7E2D8 !important;
}
.prompt-textarea {
background: #FFFFFF !important;
color: #2D2A26 !important;
border: 1px solid #E8E4DC !important;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.04) !important;
}
.prompt-textarea:focus {
border-color: #C8A96E !important;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.04), 0 0 0 2px rgba(200,169,110,0.15) !important;
}
.panel-footer {
border-top: 1px solid #E8E4DC !important;
}
.tool-prompt-select {
background: #FFFFFF !important;
color: #2D2A26 !important;
border: 1px solid #E8E4DC !important;
}
.status-text {
color: #B5AFA6 !important;
}
.btn-close-panel {
color: #B5AFA6 !important;
background: none !important;
}
.btn-close-panel:hover {
color: #EF4444 !important;
}
/* ── Feedback Bar ── */
.feedback-bar .fb-btn {
border: 1px solid #DBD7D0 !important;
color: #78716C !important;
background: transparent !important;
}
.feedback-bar .fb-btn:hover {
border-color: #C8A96E !important;
color: #C8A96E !important;
background: rgba(200,169,110,0.08) !important;
}
.feedback-bar .fb-btn.active-like {
border-color: #22C55E !important;
color: #22C55E !important;
background: rgba(34,197,94,0.08) !important;
}
.feedback-bar .fb-btn.active-dislike {
border-color: #EF4444 !important;
color: #EF4444 !important;
background: rgba(239,68,68,0.08) !important;
}
/* ── Complaint Form ── */
.complaint-form {
background: #F0ECE3 !important;
border: 1px solid #E8E4DC !important;
}
.complaint-form .cat-chip {
background: #FFFFFF !important;
border: 1px solid #DBD7D0 !important;
color: #57534E !important;
}
.complaint-form .cat-chip:hover {
border-color: #C8A96E !important;
color: #C8A96E !important;
}
.complaint-form .cat-chip.selected {
border-color: #C8A96E !important;
background: rgba(200,169,110,0.12) !important;
color: #C8A96E !important;
}
.complaint-form textarea {
background: #FFFFFF !important;
color: #2D2A26 !important;
border: 1px solid #DBD7D0 !important;
}
.complaint-form .btn-cancel {
background: #E7E2D8 !important;
color: #78716C !important;
}
.complaint-form .btn-submit {
background: linear-gradient(135deg, #C8A96E 0%, #A67C52 100%) !important;
color: white !important;
}
/* ── Message View Toggle ── */
.message-view-toggle {
background: #F0ECE3 !important;
border: 1px solid #E8E4DC !important;
}
.message-view-toggle button {
color: #78716C !important;
background: transparent !important;
}
.message-view-toggle button.active {
background: linear-gradient(135deg, #C8A96E 0%, #A67C52 100%) !important;
color: white !important;
}
.message-view-toggle button:hover:not(.active) {
background: #E7E2D8 !important;
color: #2D2A26 !important;
}
/* ── Raw JSON ── */
.raw-json-view {
background: #F7F5F0 !important;
border: 1px solid #E8E4DC !important;
}
.raw-json-view pre {
color: #44403C !important;
}
/* ── Markdown Content ── */
.filtered-content .md-content strong { color: #2D2A26 !important; }
.filtered-content .md-content em { color: #57534E !important; }
.filtered-content .md-content del { color: #9C9589 !important; }
.filtered-content .md-content code {
background: rgba(200,169,110,0.12) !important;
color: #B8954A !important;
}
.filtered-content .md-content li::marker { color: #C8A96E !important; }
.filtered-content .md-content .md-heading { color: #2D2A26 !important; }
/* ── Suggestion Chips ── */
.suggestion-chips::before {
color: #9C9589 !important;
}
.suggestion-chip {
border: 1px solid #DBD7D0 !important;
color: #57534E !important;
background: transparent !important;
}
.suggestion-chip:hover {
border-color: #C8A96E !important;
color: #C8A96E !important;
background: rgba(200,169,110,0.08) !important;
}
/* ── User Insight ── */
.user-insight {
background: #F0ECE3 !important;
border: 1px solid #E8E4DC !important;
color: #57534E !important;
}
/* ── Typing Indicator ── */
.typing-indicator {
color: #B5AFA6 !important;
}
/* ═══════════════════════════════════════════════════════
INSIGHT SIDEBAR (Shopping Journey)
═══════════════════════════════════════════════════════ */
.insight-sidebar {
background: #FFFFFF !important;
border: 1px solid #E8E4DC !important;
box-shadow: 0 4px 24px rgba(0,0,0,0.05) !important;
}
.insight-sidebar-header {
border-bottom: 1px solid #E8E4DC !important;
color: #2D2A26 !important;
}
.stage-dot.future {
background: rgba(0,0,0,0.03) !important;
border: 2px solid #DBD7D0 !important;
}
.stage-dot.past {
background: rgba(34,197,94,0.1) !important;
border: 2px solid rgba(34,197,94,0.3) !important;
}
.stage-label.future { color: #B5AFA6 !important; }
.stage-label.past { color: #22C55E !important; }
.stage-label.active { color: #16A34A !important; }
.stage-connector.empty { background: #E8E4DC !important; }
.stage-connector.filled { background: rgba(34,197,94,0.3) !important; }
.insight-fields {
border-top: 1px solid #E8E4DC !important;
}
.insight-field-key {
color: #9C9589 !important;
}
.insight-field-val {
color: #57534E !important;
}
.insight-empty {
color: #B5AFA6 !important;
}
/* ═══════════════════════════════════════════════════════
SHOPPING CART DRAWER
═══════════════════════════════════════════════════════ */
.cart-fab {
background: linear-gradient(135deg, #C8A96E, #B8954A) !important;
box-shadow: 0 4px 16px rgba(200,169,110,0.35) !important;
}
.cart-drawer {
background: #FFFFFF !important;
border-left: 1px solid #E8E4DC !important;
box-shadow: -4px 0 24px rgba(0,0,0,0.08) !important;
}
.cart-drawer-header {
border-bottom: 1px solid #E8E4DC !important;
color: #C8A96E !important;
}
.cart-drawer-header button {
color: #B5AFA6 !important;
background: none !important;
}
.cart-item {
background: #F7F5F0 !important;
}
.cart-item-name {
color: #2D2A26 !important;
}
.cart-item-price {
color: #C8A96E !important;
}
.cart-item-sku {
color: #9C9589 !important;
}
.cart-item-remove {
color: #B5AFA6 !important;
background: none !important;
}
.cart-item-remove:hover {
color: #EF4444 !important;
}
.cart-drawer-footer {
border-top: 1px solid #E8E4DC !important;
}
.cart-total {
color: #2D2A26 !important;
}
.cart-total-price {
color: #C8A96E !important;
}
.cart-send-btn {
background: linear-gradient(135deg, #C8A96E, #B8954A) !important;
}
.cart-empty {
color: #B5AFA6 !important;
}
.cart-overlay {
background: rgba(0,0,0,0.3) !important;
}
.cart-toast {
background: #C8A96E !important;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment