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

feat: refactor AI Tagging to output semantic natural language tags and fix unicode logging

parent b3888b0b
...@@ -285,18 +285,19 @@ CANIFA_LINES = [ ...@@ -285,18 +285,19 @@ CANIFA_LINES = [
] ]
# Attempt to dynamically fetch product lines from DB to override the static list # Attempt to dynamically fetch product lines from DB to override the static list
try: # Removed dynamic loading on import to prevent blocking thread when DB is slow
_conn = get_pooled_connection_compat() # try:
_cur = _conn.cursor() # _conn = get_pooled_connection_compat()
_cur.execute(f"SELECT DISTINCT product_line FROM {PG_TABLE} WHERE product_line IS NOT NULL AND product_line != ''") # _cur = _conn.cursor()
_db_lines = [r[0] for r in _cur.fetchall()] # _cur.execute(f"SELECT DISTINCT product_line FROM {PG_TABLE} WHERE product_line IS NOT NULL AND product_line != ''")
_cur.close() # _db_lines = [r[0] for r in _cur.fetchall()]
_conn.close() # _cur.close()
if _db_lines: # _conn.close()
CANIFA_LINES = _db_lines # if _db_lines:
logger.info(f"✅ Loaded {len(CANIFA_LINES)} product lines dynamically from DB") # CANIFA_LINES = _db_lines
except Exception as e: # logger.info(f"✅ Loaded {len(CANIFA_LINES)} product lines dynamically from DB")
logger.warning(f"⚠️ Could not load product lines from DB, using fallback. Error: {e}") # except Exception as e:
# logger.warning(f"⚠️ Could not load product lines from DB, using fallback. Error: {e}")
AI_SEARCH_SYSTEM = """Bạn là AI phân tích ý định tìm kiếm sản phẩm thời trang Canifa. AI_SEARCH_SYSTEM = """Bạn là AI phân tích ý định tìm kiếm sản phẩm thời trang Canifa.
...@@ -945,7 +946,7 @@ async def batch_generate_tags(): ...@@ -945,7 +946,7 @@ async def batch_generate_tags():
conn = get_pooled_connection_compat() conn = get_pooled_connection_compat()
cur = conn.cursor() cur = conn.cursor()
# Find up to 500 products that either don't have the tags column populated or it's empty # Find up to 500 products that either don't have the tags column populated or it's empty
cur.execute(f"SELECT internal_ref_code FROM {PG_TABLE} WHERE tags IS NULL OR tags = '[]'::jsonb LIMIT 500") cur.execute(f"SELECT internal_ref_code FROM {PG_TABLE} WHERE tags IS NULL LIMIT 500")
rows = cur.fetchall() rows = cur.fetchall()
if not rows: if not rows:
......
"""
Tags Direct API — Batch tagging thẳng vào PostgreSQL, không cần Redis/Worker.
- POST /api/tags-direct/run → chạy batch tagging (background asyncio task)
- GET /api/tags-direct/status → poll tiến trình
- POST /api/tags-direct/stop → dừng
- POST /api/tags-direct/single → sinh tag cho 1 sản phẩm cụ thể (sync, trả về ngay)
"""
import asyncio
import ast
import json
import logging
import re
import time
from typing import Optional
import httpx
from fastapi import APIRouter, BackgroundTasks
from fastapi.responses import JSONResponse
from common.pool_wrapper import get_pooled_connection_compat
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/tags-direct", tags=["Tags Direct"])
PG_TABLE = "dashboard_canifa.ultra_descriptions"
# ── Groq keys round-robin ──────────────────────────────────────────────────
GROQ_KEYS = [
"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",
"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",
"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",
"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",
"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",
"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",
]
_key_idx = 0
def _next_key() -> str:
global _key_idx
k = GROQ_KEYS[_key_idx % len(GROQ_KEYS)]
_key_idx += 1
return k
GROQ_MODELS = [
"openai/gpt-oss-120b",
"openai/gpt-oss-20b",
"llama-4-scout-17b-16e-instruct",
"qwen/qwen3-32b",
]
VALID_PREFIXES = ("occ:", "wthr:", "func:", "style:", "fit:")
TAG_PROMPT = """Bạn là chuyên gia gán tag thời trang cho sản phẩm Canifa.
NHIỆM VỤ:
Đọc mô tả sản phẩm bên dưới (bao gồm Tên sản phẩm chứa mã màu) và trích xuất TỐI ĐA 6 tags phù hợp nhất từ DANH SÁCH TAGS HỢP LỆ.
DANH SÁCH TAGS HỢP LỆ:
--- TRỤC 1 — Màu sắc & Tông màu ---
(Tự nhận diện màu từ tên SP. VD: "Đen/ Black" -> "Màu đen", "Xám/ Gray" -> "Màu xám")
- Màu sắc: Màu đen, Màu trắng, Màu xám, Màu xanh navy, Màu xanh da trời, Màu be, Màu nâu, Màu hồng, Màu đỏ, Màu vàng...
- Tông màu: Tông trung tính (Neutral), Tông màu pastel, Tông màu rực rỡ, Tông màu tối, Tông màu sáng.
--- TRỤC 2 — Hoàn cảnh & Dịp ---
Đi làm / Công sở, Đi chơi / dạo phố, Mặc nhà, Tập thể thao, Đi tiệc, Đi biển, Du lịch, Đi học.
--- TRỤC 3 — Mùa vụ ---
Mùa hè, Mùa đông, Cả 2 mùa, Giao mùa.
--- TRỤC 4 — Phong cách & Tính năng ---
Cơ bản (Basic), Năng động, Thanh lịch, Dễ thương, Cá tính, Tối giản, Smart Casual, Thoáng mát, Giữ ấm, Thấm hút, Nhanh khô, Chống UV, Cản gió.
--- TRỤC 5 — Dáng & Kiểu cắt ---
Oversize, Slim, Regular, Wide leg, Cropped, Relaxed.
QUY TẮC BẮT BUỘC:
1. Bắt buộc phải có tag Hoàn cảnh & Dịp, Mùa vụ (nếu có thể suy luận).
2. Nếu Tên sản phẩm có màu, ưu tiên trả về tag Màu sắc và Tông màu.
3. KHÔNG gán "Đi tiệc" cho áo phông/đồ trẻ em/đồ mặc nhà.
4. Chọn tối đa 6 tags, tối thiểu 3 tags.
5. Chỉ trả về một Python list duy nhất chứa các chuỗi (string), KHÔNG giải thích, KHÔNG markdown.
Ví dụ: ['Màu đen', 'Tông trung tính (Neutral)', 'Đi chơi / dạo phố', 'Cả 2 mùa', 'Cơ bản (Basic)']
MÔ TẢ SẢN PHẨM:
{product_description}
OUTPUT (chỉ list, không gì khác):
"""
# ── In-memory state (singleton per process) ───────────────────────────────
_state = {
"is_running": False,
"total": 0,
"done": 0,
"errors": 0,
"current_code": None,
"stop_requested": False,
"started_at": None,
"finished_at": None,
"last_errors": [], # last 10 error codes
}
_task: Optional[asyncio.Task] = None
# ── Helpers ────────────────────────────────────────────────────────────────
async def _call_ai(client: httpx.AsyncClient, text: str) -> Optional[list]:
"""Call Groq with model fallback, return list of valid tags or None."""
global _state
payload = {
"messages": [{"role": "user", "content": text}],
"temperature": 0.1,
"max_tokens": 1000,
}
for model in GROQ_MODELS:
payload["model"] = model
for _ in range(len(GROQ_KEYS)):
if _state.get("stop_requested"):
return None
key = _next_key()
try:
resp = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
json=payload,
headers={"Authorization": f"Bearer {key}"},
timeout=25.0,
)
if resp.status_code == 200:
content = resp.json()["choices"][0]["message"]["content"].strip()
# strip code fences
content = re.sub(r"^```[a-z]*\n?", "", content)
content = re.sub(r"```$", "", content).strip()
try:
result = ast.literal_eval(content)
except Exception:
try:
result = json.loads(content)
except Exception as e:
print(f"[tags-direct] Parse Error. Content: {content}")
result = []
if isinstance(result, list):
clean = [str(t) for t in result]
return clean
else:
with open("direct_tag.log", "a", encoding="utf-8") as f: f.write(f"[tags-direct] Expected list, got {type(result)}. Content: {content}\n")
return None
elif resp.status_code == 429:
match = re.search(r"try again in ([0-9.]+)s", resp.text)
wait = float(match.group(1)) if match else 10.0
with open("direct_tag.log", "a", encoding="utf-8") as f: f.write(f"[tags-direct] Rate limit (429) model={model}. Wait {wait}s.\n")
wait = min(wait, 15.0) # Cap wait at 15s
for _ in range(int(wait)):
if _state.get("stop_requested"): return None
await asyncio.sleep(1)
else:
with open("direct_tag.log", "a", encoding="utf-8") as f: f.write(f"[tags-direct] Groq Error {resp.status_code}: {resp.text}\n")
except Exception as e:
with open("direct_tag.log", "a", encoding="utf-8") as f: f.write(f"[tags-direct] Exception model={model}: {e}\n")
for _ in range(2):
if _state.get("stop_requested"): return None
await asyncio.sleep(1)
return None
def _fetch_untagged(limit: int = 2000, force_all: bool = False) -> list[tuple]:
"""Return list of (code, name, clean_description) for products."""
conn = get_pooled_connection_compat()
try:
cur = conn.cursor()
condition = "1=1" if force_all else "tags IS NULL"
cur.execute(
f"""SELECT internal_ref_code, product_name, clean_description
FROM {PG_TABLE}
WHERE {condition}
AND clean_description IS NOT NULL AND clean_description != ''
LIMIT %s""",
(limit,),
)
rows = cur.fetchall()
cur.close()
return rows
finally:
conn.close()
def _save_tags(code: str, tags: list) -> None:
conn = get_pooled_connection_compat()
try:
cur = conn.cursor()
cur.execute(
f"UPDATE {PG_TABLE} SET tags = %s::jsonb, updated_at = NOW() WHERE internal_ref_code = %s",
(json.dumps(tags, ensure_ascii=False), code),
)
conn.commit()
cur.close()
finally:
conn.close()
# ── Core batch runner ──────────────────────────────────────────────────────
async def _run_batch(force_all: bool = False):
global _state
logger.info(f"🏷️ [tags-direct] Batch started (force_all={force_all})")
try:
rows = await asyncio.get_event_loop().run_in_executor(None, _fetch_untagged, 2000, force_all)
except Exception as e:
logger.error(f"[tags-direct] DB fetch error: {e}")
_state["is_running"] = False
return
if not rows:
logger.info("[tags-direct] Không có sản phẩm nào cần gán tag.")
_state.update({"is_running": False, "total": 0, "done": 0, "finished_at": time.time()})
return
_state.update({
"total": len(rows),
"done": 0,
"errors": 0,
"last_errors": [],
"current_code": rows[0][0] if rows else None,
})
sem = asyncio.Semaphore(2) # Reduced to 2 to prevent Groq API rate limit exhaustion
async def _process(code: str, name: str, desc: str):
if _state["stop_requested"]:
return
async with sem:
try:
_state["current_code"] = code
product_text = f"Sản phẩm: {name or ''}\nMô tả:\n{desc or ''}"
prompt = TAG_PROMPT.format(product_description=product_text[:3000])
tags = await _call_ai(client, prompt)
if tags is not None:
await asyncio.get_event_loop().run_in_executor(None, _save_tags, code, tags)
_state["done"] += 1
else:
_state["errors"] += 1
_state["last_errors"].append(code)
_state["last_errors"] = _state["last_errors"][-10:]
except Exception as e:
with open("direct_tag.log", "a", encoding="utf-8") as f: f.write(f"[tags-direct] error for {code}: {e}\n")
_state["errors"] += 1
async with httpx.AsyncClient() as client:
tasks = [_process(r[0], r[1] or "", r[2] or "") for r in rows]
await asyncio.gather(*tasks)
_state.update({
"is_running": False,
"stop_requested": False,
"finished_at": time.time(),
})
logger.info(f"✅ [tags-direct] Done: {_state['done']}/{_state['total']}, errors: {_state['errors']}")
# ── Endpoints ──────────────────────────────────────────────────────────────
@router.post("/run", summary="Chạy batch gán tags thẳng vào PostgreSQL (không cần Redis)")
async def run_tags_direct(background_tasks: BackgroundTasks, force_all: bool = False):
global _state, _task
if _state["is_running"]:
return JSONResponse(
status_code=409,
content={"status": "error", "message": "Đang chạy rồi! Đợi xong hoặc bấm Stop."},
)
_state.update({
"is_running": True,
"stop_requested": False,
"started_at": time.time(),
"finished_at": None,
"total": 0,
"done": 0,
"errors": 0,
"current_code": None,
"last_errors": [],
})
background_tasks.add_task(_run_batch, force_all)
return {"status": "success", "message": "🚀 Batch tagging đã bắt đầu chạy nền!"}
@router.get("/status", summary="Poll tiến trình batch tagging")
async def tags_direct_status():
s = _state.copy()
pct = round(s["done"] / s["total"] * 100, 1) if s["total"] > 0 else 0
elapsed = round(time.time() - s["started_at"], 0) if s["started_at"] else 0
return {
"is_running": s["is_running"],
"total": s["total"],
"done": s["done"],
"errors": s["errors"],
"current_code": s["current_code"],
"percent": pct,
"elapsed_sec": elapsed,
"last_errors": s["last_errors"],
}
@router.post("/stop", summary="Dừng batch tagging")
async def stop_tags_direct():
if not _state["is_running"]:
return {"status": "ok", "message": "Không có task nào đang chạy."}
_state["stop_requested"] = True
return {"status": "ok", "message": "✋ Đã yêu cầu dừng. Các job đang chạy sẽ kết thúc sau giây lát."}
@router.post("/single", summary="Sinh tag cho 1 sản phẩm cụ thể (sync)")
async def single_tag(body: dict):
code = (body.get("internal_ref_code") or "").strip()
if not code:
return JSONResponse(status_code=400, content={"status": "error", "message": "Thiếu internal_ref_code"})
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute(
f"SELECT product_name, clean_description FROM {PG_TABLE} WHERE internal_ref_code = %s",
(code,),
)
row = cur.fetchone()
cur.close()
conn.close()
except Exception as e:
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
if not row:
return JSONResponse(status_code=404, content={"status": "error", "message": f"Không tìm thấy {code}"})
name, desc = row
prompt = TAG_PROMPT.format(product_description=f"Sản phẩm: {name or ''}\nMô tả:\n{(desc or '')[:3000]}")
async with httpx.AsyncClient() as client:
tags = await _call_ai(client, prompt)
if not tags:
return JSONResponse(status_code=500, content={"status": "error", "message": "AI không trả về tags hợp lệ"})
try:
_save_tags(code, tags)
except Exception as e:
return JSONResponse(status_code=500, content={"status": "error", "message": f"Lưu DB lỗi: {e}"})
return {"status": "success", "code": code, "tags": tags}
@router.get("/count-untagged", summary="Đếm số sản phẩm chưa có tag")
async def count_untagged():
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute(
f"SELECT COUNT(*) FROM {PG_TABLE} WHERE tags IS NULL"
)
count = cur.fetchone()[0]
cur.close()
conn.close()
return {"status": "success", "untagged": count}
except Exception as e:
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
...@@ -28,52 +28,61 @@ class UltraDescriptionDB: ...@@ -28,52 +28,61 @@ class UltraDescriptionDB:
"""Create ultra_descriptions table if it doesn't exist.""" """Create ultra_descriptions table if it doesn't exist."""
if cls._initialized: if cls._initialized:
return return
from config import USE_LOCAL_SQLITE
conn = None conn = None
try: try:
conn = get_pooled_connection_compat() conn = get_pooled_connection_compat()
cur = conn.cursor() cur = conn.cursor()
cur.execute(f"""
CREATE TABLE IF NOT EXISTS {TABLE} ( if USE_LOCAL_SQLITE:
id SERIAL PRIMARY KEY, # SQLite: Create full table at once with all columns
internal_ref_code VARCHAR(50) NOT NULL, cur.execute(f"""
product_name VARCHAR(500), CREATE TABLE IF NOT EXISTS {TABLE} (
product_image_url TEXT, id INTEGER PRIMARY KEY AUTOINCREMENT,
product_line VARCHAR(200), internal_ref_code TEXT NOT NULL,
description_data JSONB NOT NULL, magento_ref_code TEXT,
phase VARCHAR(20) DEFAULT 'enriched', base_ref_code TEXT,
status SMALLINT DEFAULT 0, product_name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), product_image_url TEXT,
updated_at TIMESTAMPTZ DEFAULT NOW() product_line TEXT,
); description_data TEXT NOT NULL,
CREATE INDEX IF NOT EXISTS idx_ultra_desc_ref_code phase TEXT DEFAULT 'enriched',
ON {TABLE}(internal_ref_code); status INTEGER DEFAULT 0,
-- Migration: add columns if table already existed clean_description TEXT DEFAULT '',
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS status SMALLINT DEFAULT 0; tags TEXT DEFAULT '[]',
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS clean_description TEXT DEFAULT ''; ai_matches TEXT,
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS tags JSONB DEFAULT '[]'::jsonb; created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Migration v2: magento_ref_code support updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS magento_ref_code VARCHAR(100); );
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS base_ref_code VARCHAR(50); """)
""") else:
# Drop old unique constraint on internal_ref_code (v2 migration: allow multiple colors per style) # PostgreSQL: Original logic
try: cur.execute(f"""
cur.execute(f"ALTER TABLE {TABLE} DROP CONSTRAINT IF EXISTS ultra_descriptions_internal_ref_code_key") CREATE TABLE IF NOT EXISTS {TABLE} (
except Exception: id SERIAL PRIMARY KEY,
pass internal_ref_code VARCHAR(50) NOT NULL,
# Non-unique index on internal_ref_code (for grouping) product_name VARCHAR(500),
try: product_image_url TEXT,
cur.execute(f"CREATE INDEX IF NOT EXISTS idx_ultra_desc_internal_code ON {TABLE}(internal_ref_code)") product_line VARCHAR(200),
except Exception: description_data JSONB NOT NULL,
pass phase VARCHAR(20) DEFAULT 'enriched',
# Unique index on magento_ref_code (partial, ignores NULLs) status SMALLINT DEFAULT 0,
try: created_at TIMESTAMPTZ DEFAULT NOW(),
cur.execute(f"""CREATE UNIQUE INDEX IF NOT EXISTS idx_ultra_desc_magento_code updated_at TIMESTAMPTZ DEFAULT NOW()
ON {TABLE}(magento_ref_code) WHERE magento_ref_code IS NOT NULL;""") );
except Exception: ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS status SMALLINT DEFAULT 0;
pass # already exists ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS clean_description TEXT DEFAULT '';
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS tags JSONB DEFAULT '[]'::jsonb;
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS magento_ref_code VARCHAR(100);
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS base_ref_code VARCHAR(50);
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS ai_matches JSONB;
""")
cur.close() cur.close()
conn.commit()
cls._initialized = True cls._initialized = True
logger.info("✅ Table %s ready", TABLE) logger.info("✅ Table %s ready (Mock: %s)", TABLE, USE_LOCAL_SQLITE)
except Exception as e: except Exception as e:
logger.error("Error creating ultra_descriptions table: %s", e) logger.error("Error creating ultra_descriptions table: %s", e)
finally: finally:
...@@ -134,9 +143,9 @@ class UltraDescriptionDB: ...@@ -134,9 +143,9 @@ class UltraDescriptionDB:
json.dumps(description_data, ensure_ascii=False), phase, clean_description), json.dumps(description_data, ensure_ascii=False), phase, clean_description),
) )
row = cur.fetchone() row = cur.fetchone()
cur.close()
row_id = row[0] if row else None row_id = row[0] if row else None
conn.commit() # critical: without this INSERT/UPDATE rolls back on conn.close() cur.close() # Close cursor FIRST
conn.commit()
logger.info("💾 Saved ultra desc: %s / magento=%s (id=%s)", internal_ref_code, magento_ref_code, row_id) logger.info("💾 Saved ultra desc: %s / magento=%s (id=%s)", internal_ref_code, magento_ref_code, row_id)
return row_id return row_id
except Exception as e: except Exception as e:
...@@ -565,7 +574,7 @@ class UltraDescriptionDB: ...@@ -565,7 +574,7 @@ class UltraDescriptionDB:
try: try:
conn = get_pooled_connection_compat() conn = get_pooled_connection_compat()
cur = conn.cursor() cur = conn.cursor()
cur.execute(f"SELECT COUNT(*) FROM {TABLE} WHERE tags IS NULL OR tags = '[]'::jsonb") cur.execute(f"SELECT COUNT(*) FROM {TABLE} WHERE tags IS NULL")
count = cur.fetchone()[0] count = cur.fetchone()[0]
cur.close() cur.close()
return count return count
...@@ -618,11 +627,11 @@ class UltraDescriptionDB: ...@@ -618,11 +627,11 @@ class UltraDescriptionDB:
conn.close() conn.close()
# Auto-init table on import # Removed auto-init on import to prevent blocking thread when DB is slow
try: # try:
UltraDescriptionDB.ensure_table() # UltraDescriptionDB.ensure_table()
except Exception: # except Exception:
pass # pass
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
...@@ -848,8 +857,8 @@ class DescFieldConfig: ...@@ -848,8 +857,8 @@ class DescFieldConfig:
conn.close() conn.close()
# Auto-init field config table # Removed auto-init on import to prevent blocking thread when DB is slow
try: # try:
DescFieldConfig.ensure_table() # DescFieldConfig.ensure_table()
except Exception: # except Exception:
pass # pass
import re
from collections import Counter
with open('test_db.magento_product_dimension_with_text_embedding.sql', encoding='utf-8', errors='ignore') as f:
data = f.read()
# Find the column order from INSERT statement
col_match = re.search(r'INSERT INTO[^(]+\(([^)]+)\)', data, re.DOTALL)
if col_match:
cols = [c.strip().strip('`') for c in col_match.group(1).split(',')]
season_idx = cols.index('season') if 'season' in cols else -1
print(f"Column order found. 'season' is at index: {season_idx}")
print(f"Columns around season: {cols[max(0,season_idx-2):season_idx+3]}")
else:
print("Could not find INSERT column list")
season_idx = -1
# Extract VALUES rows - find all value tuples
# Look for patterns like ('...', '...', NULL, ...)
rows = re.findall(r'\(([^;]+?)\)(?:,|\s*;)', data, re.DOTALL)
print(f"\nTotal value rows found: {len(rows)}")
if season_idx >= 0 and rows:
seasons = []
for row in rows[:50]: # sample first 50
# Split by comma but respect quoted strings
parts = re.split(r",(?=(?:[^']*'[^']*')*[^']*$)", row.strip())
if len(parts) > season_idx:
val = parts[season_idx].strip().strip("'")
seasons.append(val)
counts = Counter(seasons)
print(f"\nSeason values (sample from first 50 rows):")
print("-" * 40)
for s, c in sorted(counts.items(), key=lambda x: -x[1]):
print(f"{c:5d} | '{s}'")
<!DOCTYPE html> <!DOCTYPE html>
<html lang="vi"> <html lang="vi">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ultra Description — Canifa AI</title> <title>Ultra Description — Canifa AI</title>
<link rel="stylesheet" href="/static/common/theme.css"> <link rel="stylesheet" href="/static/common/theme.css">
<link rel="stylesheet" href="/static/common/components.css"> <link rel="stylesheet" href="/static/common/components.css">
<script src="/static/common/frame-detect.js"></script> <script src="/static/common/frame-detect.js"></script>
<link rel="stylesheet" href="/static/product-desc/product-desc.css"> <link rel="stylesheet" href="/static/product-desc/product-desc.css">
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<!-- Header --> <!-- Header -->
<div class="page-hdr" style="margin-bottom:10px;"> <div class="page-hdr" style="margin-bottom:10px;">
<div> <div>
<h1>✦ Ultra Description Manager</h1> <h1>✦ Ultra Description Manager</h1>
<p>Sinh mô tả sản phẩm AI — kết hợp Vision + Stock Data</p> <p>Sinh mô tả sản phẩm AI — kết hợp Vision + Stock Data</p>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-outline btn-sm"
onclick="let a=document.createElement('a'); a.setAttribute('data-page','product-desc/docs.html'); window.parent.navigateTo(a)"
title="Xem tài liệu hệ thống">📄 Tài liệu</button>
<button class="btn btn-outline btn-sm"
onclick="loadOverview();if(currentTab==='products') loadProducts(); else loadFields();">↻ Refresh</button>
</div>
</div> </div>
<div style="display:flex;gap:8px;">
<button class="btn btn-outline btn-sm" onclick="loadOverview();if(currentTab==='products') loadProducts(); else loadFields();">↻ Refresh</button> <!-- Tabs -->
<div style="display:flex; gap:20px; border-bottom:1px solid var(--border); margin-bottom:20px;">
<div class="tab-btn active" id="tabProducts" onclick="switchTab('products')"
style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid var(--primary); color:var(--primary);">
Sản phẩm</div>
<div class="tab-btn" id="tabFields" onclick="switchTab('fields')"
style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid transparent; color:var(--muted-fg);">
Quản lý Trường</div>
<div class="tab-btn" id="tabCleanData" onclick="switchTab('cleanData')"
style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid transparent; color:var(--muted-fg);">
Clean Data</div>
</div> </div>
</div>
<!-- Tabs --> <div id="sectionProducts">
<div style="display:flex; gap:20px; border-bottom:1px solid var(--border); margin-bottom:20px;"> <!-- Stats -->
<div class="tab-btn active" id="tabProducts" onclick="switchTab('products')" style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid var(--primary); color:var(--primary);">Sản phẩm</div> <div class="stats-row" id="statsRow">
<div class="tab-btn" id="tabFields" onclick="switchTab('fields')" style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid transparent; color:var(--muted-fg);">Quản lý Trường</div> <div class="stat-card">
<div class="tab-btn" id="tabCleanData" onclick="switchTab('cleanData')" style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid transparent; color:var(--muted-fg);">Clean Data</div> <div class="label">Tổng Magento Code</div>
</div> <div class="value primary" id="statTotal"></div>
<div class="sub" id="statTotalSub">Theo StarRocks catalog</div>
</div>
<div class="stat-card">
<div class="label">Đã generate</div>
<div class="value" id="statDone"></div>
<div class="sub">Rows trong DB (per magento code)</div>
</div>
<div class="stat-card">
<div class="label">✅ Đã duyệt</div>
<div class="value success" id="statApproved"></div>
</div>
<div class="stat-card">
<div class="label">⏳ Chờ duyệt</div>
<div class="value" style="color:#856404" id="statPending"></div>
</div>
<div class="stat-card">
<div class="label">Chưa có mô tả</div>
<div class="value warn" id="statMissing"></div>
<div class="sub">Magento code chưa có trong DB</div>
</div>
<div class="stat-card">
<div class="label">Tiến độ duyệt</div>
<div class="value" id="statProgress"></div>
<div class="progress-bar-wrap">
<div class="progress-bar-fill" id="progressFill" style="width:0%"></div>
</div>
</div>
<div class="stat-card">
<div class="label">Thiếu Clean Desc</div>
<div class="value warn" id="statMissingClean"></div>
<div class="sub" id="statMissingCleanSub">Cần render lại theo format chuẩn</div>
</div>
<div class="stat-card">
<div class="label">Đã Có Clean Desc</div>
<div class="value success" id="statHasClean"></div>
<div class="sub">Đếm theo cột `clean_description` trong DB</div>
</div>
<div class="stat-card">
<div class="label">Chưa Có Tags</div>
<div class="value warn" id="statMissingTags"></div>
<div class="sub">Sản phẩm cần bơm AI Tags</div>
</div>
<div class="stat-card" style="border-left:3px solid #7c3aed">
<div class="label" style="color:#7c3aed">📏 Đã Có Size Guide</div>
<div class="value" style="color:#7c3aed" id="statSizeGuide"></div>
<div class="sub">SP đã được AI sinh hướng dẫn size</div>
</div>
</div>
<div id="sectionProducts"> <!-- Filters -->
<!-- Stats --> <div class="filter-bar">
<div class="stats-row" id="statsRow"> <input type="text" class="search-input" id="searchInput" placeholder="Tìm theo tên hoặc mã sản phẩm...">
<div class="stat-card"> <select id="statusFilter">
<div class="label">Tổng Magento Code</div> <option value="">Tất cả</option>
<div class="value primary" id="statTotal"></div> <option value="missing">✕ Chưa có mô tả</option>
<div class="sub" id="statTotalSub">Theo StarRocks catalog</div> <option value="pending">⏳ Chờ duyệt</option>
</div> <option value="approved">✅ Đã duyệt</option>
<div class="stat-card"> <option value="has_desc">Đã generate (tất cả)</option>
<div class="label">Đã generate</div> </select>
<div class="value" id="statDone"></div> <select id="lineFilter">
<div class="sub">Rows trong DB (per magento code)</div> <option value="">Tất cả dòng SP</option>
</div> </select>
<div class="stat-card"> <select id="tagsFilter" class="search-input" style="width: 220px;" onchange="loadProducts()">
<div class="label">✅ Đã duyệt</div> <option value="">Lọc theo Tag DB (Tất cả)</option>
<div class="value success" id="statApproved"></div> <optgroup label="Hoàn cảnh (Occasion)">
</div> <option value="occ:di_lam">occ:di_lam (Đi làm)</option>
<div class="stat-card"> <option value="occ:di_choi">occ:di_choi (Đi chơi)</option>
<div class="label">⏳ Chờ duyệt</div> <option value="occ:di_tiec">occ:di_tiec (Đi tiệc)</option>
<div class="value" style="color:#856404" id="statPending"></div> <option value="occ:di_hoc">occ:di_hoc (Đi học)</option>
</div> <option value="occ:mac_nha">occ:mac_nha (Mặc nhà)</option>
<div class="stat-card"> <option value="occ:the_thao">occ:the_thao (Thể thao)</option>
<div class="label">Chưa có mô tả</div> <option value="occ:di_bien">occ:di_bien (Đi biển)</option>
<div class="value warn" id="statMissing"></div> <option value="occ:du_lich">occ:du_lich (Du lịch)</option>
<div class="sub">Magento code chưa có trong DB</div> <option value="occ:da_ngoai">occ:da_ngoai (Dã ngoại)</option>
</div> <option value="occ:di_ngu">occ:di_ngu (Đi ngủ)</option>
<div class="stat-card"> </optgroup>
<div class="label">Tiến độ duyệt</div> <optgroup label="Thời tiết (Weather)">
<div class="value" id="statProgress"></div> <option value="wthr:mua_he">wthr:mua_he (Mùa hè)</option>
<div class="progress-bar-wrap"><div class="progress-bar-fill" id="progressFill" style="width:0%"></div></div> <option value="wthr:mua_dong">wthr:mua_dong (Mùa đông)</option>
</div> <option value="wthr:giao_mua">wthr:giao_mua (Giao mùa)</option>
<div class="stat-card"> <option value="wthr:troi_mua">wthr:troi_mua (Trời mưa)</option>
<div class="label">Thiếu Clean Desc</div> <option value="wthr:troi_nang">wthr:troi_nang (Trời nắng)</option>
<div class="value warn" id="statMissingClean"></div> </optgroup>
<div class="sub" id="statMissingCleanSub">Cần render lại theo format chuẩn</div> <optgroup label="Tính năng (Function)">
</div> <option value="func:thoang_mat">func:thoang_mat (Thoáng mát)</option>
<div class="stat-card"> <option value="func:giu_am">func:giu_am (Giữ ấm)</option>
<div class="label">Đã Có Clean Desc</div> <option value="func:tham_hut">func:tham_hut (Thấm hút)</option>
<div class="value success" id="statHasClean"></div> <option value="func:nhanh_kho">func:nhanh_kho (Nhanh khô)</option>
<div class="sub">Đếm theo cột `clean_description` trong DB</div> <option value="func:chong_uv">func:chong_uv (Chống uv)</option>
</div> <option value="func:can_gio">func:can_gio (Cản gió)</option>
<div class="stat-card"> </optgroup>
<div class="label">Chưa Có Tags</div> <optgroup label="Phong cách (Style)">
<div class="value warn" id="statMissingTags"></div> <option value="style:thanh_lich">style:thanh_lich (Thanh lịch)</option>
<div class="sub">Sản phẩm cần bơm AI Tags</div> <option value="style:nang_dong">style:nang_dong (Năng động)</option>
</div> <option value="style:basic">style:basic (Basic)</option>
<div class="stat-card" style="border-left:3px solid #7c3aed"> <option value="style:ca_tinh">style:ca_tinh (Cá tính)</option>
<div class="label" style="color:#7c3aed">📏 Đã Có Size Guide</div> <option value="style:de_thuong">style:de_thuong (Dễ thương)</option>
<div class="value" style="color:#7c3aed" id="statSizeGuide"></div> <option value="style:tre_trung">style:tre_trung (Trẻ trung)</option>
<div class="sub">SP đã được AI sinh hướng dẫn size</div> <option value="style:toi_gian">style:toi_gian (Tối giản)</option>
</div> <option value="style:smart_casual">style:smart_casual (Smart casual)</option>
</div> </optgroup>
<optgroup label="Dáng (Fit)">
<option value="fit:oversize">fit:oversize (Oversize)</option>
<option value="fit:slim">fit:slim (Slim)</option>
<option value="fit:regular">fit:regular (Regular)</option>
<option value="fit:wide_leg">fit:wide_leg (Ống rộng)</option>
<option value="fit:cropped">fit:cropped (Cropped)</option>
<option value="fit:relaxed">fit:relaxed (Relaxed)</option>
</optgroup>
</select>
<div class="filter-actions">
<button class="btn btn-outline-dark btn-sm" onclick="loadProducts()">Lọc</button>
<button class="btn btn-outline-dark btn-sm" onclick="resetFilters()">Reset</button>
<button class="btn btn-dark btn-sm" id="btnApprovePage" onclick="batchApprovePage()">Duyệt (Trang
này)</button>
<button class="btn btn-dark btn-sm" id="btnApproveAll" style="font-weight:600;" onclick="approveAll()">Duyệt
TOÀN BỘ</button>
<button class="btn btn-outline-dark btn-sm" id="btnBackfillClean" style="font-weight:600;"
onclick="backfillMissingCleanDescriptions()">Bù Clean Desc</button>
<button class="btn btn-outline-dark btn-sm" id="btnBatch" onclick="batchGeneratePage()">Sinh AI (Trang
này)</button>
<button class="btn btn-outline-dark btn-sm" id="btnBatchAll" onclick="batchGenerateAll()">Sinh AI (Toàn bộ
thiếu)</button>
<button class="btn btn-dark btn-sm" id="btnBatchTags" onclick="batchGenerateTags()">Bơm Tags (Queue)</button>
<button class="btn btn-sm" id="btnBatchTagsDirect" onclick="runTagsDirect()"
style="background:#059669;color:#fff;border:none;font-weight:700;">🏷️ Bơm Tags TOÀN BỘ (Direct)</button>
<button class="btn btn-sm" id="btnBatchSize"
style="background:#7c3aed;color:#fff;border:none;font-weight:600;" onclick="batchEnrichSize()">📏 Bơm Size
Guide</button>
<button class="btn btn-primary btn-sm" style="font-weight:bold; background:#0284c7; border-color:#0284c7;"
onclick="openNativeBulkAiModal()">🤖 Bulk AI Edit (Custom)</button>
</div>
</div><!-- Batch Tracking Bar -->
<div id="batchTrackingBox"
style="display:none; background:#f8fafc; border:1px solid #cbd5e1; border-radius:8px; padding:12px 20px; margin-bottom:20px; display:flex; gap:20px; align-items:center; box-shadow:0 2px 4px rgba(0,0,0,0.05);">
<div style="font-size:24px; filter: grayscale(1);">🤖</div>
<div style="flex:1;">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
<strong id="batchTrackTitle" style="color:#0369a1;">Hệ thống đang chạy AI hàng loạt...</strong>
<span id="batchTrackText" style="color:#0284c7; font-weight:600;">0 / 0</span>
</div>
<div style="background:rgba(255,255,255,0.7); height:8px; border-radius:4px; overflow:hidden;">
<div id="batchTrackFill" style="background:#0ea5e9; height:100%; width:0%; transition:width 0.3s;"></div>
</div>
<div style="margin-top:6px; font-size:12px; color:#0369a1;">Mã đang xử lý: <span id="batchTrackCode"
style="font-weight:600;"></span> | Lỗi: <span id="batchTrackErrors"
style="color:#ef4444; font-weight:bold;">0</span></div>
</div>
</div>
<!-- Filters --> <!-- Tags Batch Tracking Box -->
<div class="filter-bar"> <div id="tagsTrackingBox"
<input type="text" class="search-input" id="searchInput" placeholder="Tìm theo tên hoặc mã sản phẩm..."> style="display:none; background:#f8fafc; border:1px solid #cbd5e1; border-radius:8px; padding:12px 20px; margin-bottom:20px; display:flex; gap:20px; align-items:center; box-shadow:0 2px 4px rgba(0,0,0,0.05);">
<select id="statusFilter"> <div style="font-size:24px; filter: grayscale(1);">🏷️</div>
<option value="">Tất cả</option> <div style="flex:1;">
<option value="missing">✕ Chưa có mô tả</option> <div style="display:flex; justify-content:space-between; margin-bottom:6px;">
<option value="pending">⏳ Chờ duyệt</option> <strong id="tagsTrackTitle" style="color:#047857;">Hệ thống đang Auto-Tagging hàng loạt...</strong>
<option value="approved">✅ Đã duyệt</option> <span id="tagsTrackText" style="color:#059669; font-weight:600;">0 / 0</span>
<option value="has_desc">Đã generate (tất cả)</option> </div>
</select> <div style="background:rgba(255,255,255,0.7); height:8px; border-radius:4px; overflow:hidden;">
<select id="lineFilter"> <div id="tagsTrackFill" style="background:#10b981; height:100%; width:0%; transition:width 0.3s;"></div>
<option value="">Tất cả dòng SP</option> </div>
</select> <div style="margin-top:6px; font-size:12px; color:#047857;">Mã đang phân tích: <span id="tagsTrackCode"
<select id="tagsFilter" class="search-input" style="width: 220px;" onchange="loadProducts()"> style="font-weight:600;"></span> | Lỗi AI: <span id="tagsTrackErrors"
<option value="">Lọc theo Tag DB (Tất cả)</option> style="color:#ef4444; font-weight:bold;">0</span></div>
<optgroup label="Hoàn cảnh (Occasion)"> </div>
<option value="occ:di_lam">occ:di_lam (Đi làm)</option>
<option value="occ:di_choi">occ:di_choi (Đi chơi)</option>
<option value="occ:di_tiec">occ:di_tiec (Đi tiệc)</option>
<option value="occ:di_hoc">occ:di_hoc (Đi học)</option>
<option value="occ:mac_nha">occ:mac_nha (Mặc nhà)</option>
<option value="occ:the_thao">occ:the_thao (Thể thao)</option>
<option value="occ:di_bien">occ:di_bien (Đi biển)</option>
<option value="occ:du_lich">occ:du_lich (Du lịch)</option>
<option value="occ:da_ngoai">occ:da_ngoai (Dã ngoại)</option>
<option value="occ:di_ngu">occ:di_ngu (Đi ngủ)</option>
</optgroup>
<optgroup label="Thời tiết (Weather)">
<option value="wthr:mua_he">wthr:mua_he (Mùa hè)</option>
<option value="wthr:mua_dong">wthr:mua_dong (Mùa đông)</option>
<option value="wthr:giao_mua">wthr:giao_mua (Giao mùa)</option>
<option value="wthr:troi_mua">wthr:troi_mua (Trời mưa)</option>
<option value="wthr:troi_nang">wthr:troi_nang (Trời nắng)</option>
</optgroup>
<optgroup label="Tính năng (Function)">
<option value="func:thoang_mat">func:thoang_mat (Thoáng mát)</option>
<option value="func:giu_am">func:giu_am (Giữ ấm)</option>
<option value="func:tham_hut">func:tham_hut (Thấm hút)</option>
<option value="func:nhanh_kho">func:nhanh_kho (Nhanh khô)</option>
<option value="func:chong_uv">func:chong_uv (Chống uv)</option>
<option value="func:can_gio">func:can_gio (Cản gió)</option>
</optgroup>
<optgroup label="Phong cách (Style)">
<option value="style:thanh_lich">style:thanh_lich (Thanh lịch)</option>
<option value="style:nang_dong">style:nang_dong (Năng động)</option>
<option value="style:basic">style:basic (Basic)</option>
<option value="style:ca_tinh">style:ca_tinh (Cá tính)</option>
<option value="style:de_thuong">style:de_thuong (Dễ thương)</option>
<option value="style:tre_trung">style:tre_trung (Trẻ trung)</option>
<option value="style:toi_gian">style:toi_gian (Tối giản)</option>
<option value="style:smart_casual">style:smart_casual (Smart casual)</option>
</optgroup>
<optgroup label="Dáng (Fit)">
<option value="fit:oversize">fit:oversize (Oversize)</option>
<option value="fit:slim">fit:slim (Slim)</option>
<option value="fit:regular">fit:regular (Regular)</option>
<option value="fit:wide_leg">fit:wide_leg (Ống rộng)</option>
<option value="fit:cropped">fit:cropped (Cropped)</option>
<option value="fit:relaxed">fit:relaxed (Relaxed)</option>
</optgroup>
</select>
<div class="filter-actions">
<button class="btn btn-outline-dark btn-sm" onclick="loadProducts()">Lọc</button>
<button class="btn btn-outline-dark btn-sm" onclick="resetFilters()">Reset</button>
<button class="btn btn-dark btn-sm" id="btnApprovePage" onclick="batchApprovePage()">Duyệt (Trang này)</button>
<button class="btn btn-dark btn-sm" id="btnApproveAll" style="font-weight:600;" onclick="approveAll()">Duyệt TOÀN BỘ</button>
<button class="btn btn-outline-dark btn-sm" id="btnBackfillClean" style="font-weight:600;" onclick="backfillMissingCleanDescriptions()">Bù Clean Desc</button>
<button class="btn btn-outline-dark btn-sm" id="btnBatch" onclick="batchGeneratePage()">Sinh AI (Trang này)</button>
<button class="btn btn-outline-dark btn-sm" id="btnBatchAll" onclick="batchGenerateAll()">Sinh AI (Toàn bộ thiếu)</button>
<button class="btn btn-dark btn-sm" id="btnBatchTags" onclick="batchGenerateTags()">Bơm Tags (Toàn bộ)</button>
<button class="btn btn-sm" id="btnBatchSize" style="background:#7c3aed;color:#fff;border:none;font-weight:600;" onclick="batchEnrichSize()">📏 Bơm Size Guide</button>
<button class="btn btn-primary btn-sm" style="font-weight:bold; background:#0284c7; border-color:#0284c7;" onclick="openNativeBulkAiModal()">🤖 Bulk AI Edit (Custom)</button>
</div>
</div><!-- Batch Tracking Bar -->
<div id="batchTrackingBox" style="display:none; background:#f8fafc; border:1px solid #cbd5e1; border-radius:8px; padding:12px 20px; margin-bottom:20px; display:flex; gap:20px; align-items:center; box-shadow:0 2px 4px rgba(0,0,0,0.05);">
<div style="font-size:24px; filter: grayscale(1);">🤖</div>
<div style="flex:1;">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
<strong id="batchTrackTitle" style="color:#0369a1;">Hệ thống đang chạy AI hàng loạt...</strong>
<span id="batchTrackText" style="color:#0284c7; font-weight:600;">0 / 0</span>
</div> </div>
<div style="background:rgba(255,255,255,0.7); height:8px; border-radius:4px; overflow:hidden;">
<div id="batchTrackFill" style="background:#0ea5e9; height:100%; width:0%; transition:width 0.3s;"></div> <!-- Direct Tags Progress Box -->
<div id="directTagsBox"
style="display:none; background:#ecfdf5; border:2px solid #059669; border-radius:8px; padding:12px 20px; margin-bottom:20px; gap:20px; align-items:center; box-shadow:0 2px 8px rgba(5,150,105,0.15);">
<div style="display:flex; align-items:center; gap:16px;">
<div style="font-size:28px;">🚀</div>
<div style="flex:1;">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
<strong id="directTagsTitle" style="color:#065f46; font-size:14px;">🏷️ Direct Tagging đang chạy thẳng vào
PostgreSQL...</strong>
<span id="directTagsCount" style="color:#059669; font-weight:700; font-size:14px;">0 / 0</span>
</div>
<div style="background:#d1fae5; height:10px; border-radius:5px; overflow:hidden;">
<div id="directTagsFill"
style="background:linear-gradient(90deg,#10b981,#059669); height:100%; width:0%; transition:width 0.4s ease;">
</div>
</div>
<div style="margin-top:6px; font-size:12px; color:#065f46; display:flex; gap:16px;">
<span>Mã hiện tại: <strong id="directTagsCode"></strong></span>
<span>Lỗi: <strong id="directTagsErrors" style="color:#ef4444;">0</strong></span>
<span>% hoàn thành: <strong id="directTagsPct">0%</strong></span>
<button onclick="stopTagsDirect()"
style="margin-left:auto; font-size:11px; padding:2px 10px; background:#ef4444; color:#fff; border:none; border-radius:4px; cursor:pointer;">
Dừng</button>
</div>
</div>
</div>
</div> </div>
<div style="margin-top:6px; font-size:12px; color:#0369a1;">Mã đang xử lý: <span id="batchTrackCode" style="font-weight:600;"></span> | Lỗi: <span id="batchTrackErrors" style="color:#ef4444; font-weight:bold;">0</span></div>
</div> <!-- Table -->
</div> <div style="border-radius:10px; overflow:hidden; border:1px solid var(--border);">
<table class="product-table">
<!-- Tags Batch Tracking Box --> <thead>
<div id="tagsTrackingBox" style="display:none; background:#f8fafc; border:1px solid #cbd5e1; border-radius:8px; padding:12px 20px; margin-bottom:20px; display:flex; gap:20px; align-items:center; box-shadow:0 2px 4px rgba(0,0,0,0.05);"> <tr>
<div style="font-size:24px; filter: grayscale(1);">🏷️</div> <th style="width:25%">Sản phẩm</th>
<div style="flex:1;"> <th>Dòng SP</th>
<div style="display:flex; justify-content:space-between; margin-bottom:6px;"> <th>Tags</th>
<strong id="tagsTrackTitle" style="color:#047857;">Hệ thống đang Auto-Tagging hàng loạt...</strong> <th>Giá</th>
<span id="tagsTrackText" style="color:#059669; font-weight:600;">0 / 0</span>
<th>Giá</th>
<th>Lượt bán</th>
<th>Trạng thái</th>
<th>Size</th>
<th>Cập nhật</th>
<th>Duyệt</th>
<th style="width:100px">Thao tác</th>
</tr>
</thead>
<tbody id="tableBody">
<tr>
<td colspan="10" class="table-loading">Đang tải...</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<!-- Pagination -->
<div class="pagination" id="pagination"></div>
</div> <!-- end sectionProducts -->
<!-- Fields Section -->
<div id="sectionFields" style="display:none;">
<div style="display:flex; justify-content:space-between; margin-bottom:12px;">
<div>
<h2 style="margin:0; font-size:18px;">Cấu hình Prompt Fields</h2>
<p style="margin:4px 0 0; font-size:13px; color:var(--muted-fg);">Các trường này sẽ được sinh động trong quá
trình AI phân tích sản phẩm (Phase 1 & Phase 2).</p>
</div>
<button class="btn btn-primary" onclick="showFieldForm()"><span style="margin-right:6px"></span> Thêm
Trường</button>
</div> </div>
<div style="background:rgba(255,255,255,0.7); height:8px; border-radius:4px; overflow:hidden;">
<div id="tagsTrackFill" style="background:#10b981; height:100%; width:0%; transition:width 0.3s;"></div> <div style="border-radius:10px; overflow:hidden; border:1px solid var(--border); background:var(--card);">
<table class="product-table" id="fieldsTable">
<thead style="background:var(--muted);">
<tr>
<th style="width:15%">Key</th>
<th style="width:20%">Label</th>
<th style="width:40%">Instruction (Prompt)</th>
<th>Trạng thái</th>
<th style="width:120px">Thao tác</th>
</tr>
</thead>
<tbody id="fieldsTableBody">
<tr>
<td colspan="5" class="table-loading">Đang tải...</td>
</tr>
</tbody>
</table>
</div> </div>
<div style="margin-top:6px; font-size:12px; color:#047857;">Mã đang phân tích: <span id="tagsTrackCode" style="font-weight:600;"></span> | Lỗi AI: <span id="tagsTrackErrors" style="color:#ef4444; font-weight:bold;">0</span></div>
</div> </div>
</div> </div>
<!-- Table --> <!-- INLINE EDIT POPUP -->
<div style="border-radius:10px; overflow:hidden; border:1px solid var(--border);"> <div id="inlineEditPopup">
<table class="product-table"> <div
<thead> style="padding: 8px 10px; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px;">
<tr> AI Editor (Codex)</div>
<th style="width:25%">Sản phẩm</th> <button class="inline-edit-btn" onclick="triggerInlineEdit('enrich')"><i class="fas fa-plus-circle"
<th>Dòng SP</th> style="color: #10b981;"></i> Bổ sung gợi ý dịp mặc/lễ hội</button>
<th>Tags</th> <button class="inline-edit-btn" onclick="triggerInlineEdit('rewrite')"><i class="fas fa-pen"
<th>Giá</th> style="color: #3b82f6;"></i> Viết lại chuyên nghiệp hơn</button>
<th>Lượt bán</th> <button class="inline-edit-btn" onclick="triggerInlineEdit('shorten')"><i class="fas fa-compress-alt"
<th>Trạng thái</th> style="color: #8b5cf6;"></i> Viết ngắn gọn lại</button>
<th>Size</th> <button class="inline-edit-btn" onclick="triggerInlineEdit('fix')"><i class="fas fa-wrench"
<th>Cập nhật</th> style="color: #ef4444;"></i> Sửa lỗi chính tả</button>
<th>Duyệt</th>
<th style="width:100px">Thao tác</th>
</tr>
</thead>
<tbody id="tableBody">
<tr><td colspan="10" class="table-loading">Đang tải...</td></tr>
</tbody>
</table>
</div> </div>
<!-- Pagination --> <!-- Detail Overlay -->
<!-- Pagination --> <div class="detail-overlay" id="detailOverlay" onclick="if(event.target===this)closeDetail()">
<div class="pagination" id="pagination"></div> <div class="detail-panel" style="width:800px;">
</div> <!-- end sectionProducts --> <div class="detail-header" style="flex-direction:column; align-items:stretch; gap:12px; padding-bottom:0;">
<div style="display:flex; align-items:center; justify-content:space-between;">
<h2 id="detailTitle">Chi tiết mô tả</h2>
<div style="display:flex;gap:6px;">
<button class="btn btn-sm" id="btnApprove"
style="background:#28a745;color:#fff;border:none;display:none;font-weight:500"
onclick="approveDetail(1)">Duyệt</button>
<button class="btn btn-sm" id="btnReject"
style="background:#dc3545;color:#fff;border:none;display:none;font-weight:500"
onclick="approveDetail(0)">Hủy duyệt</button>
<button class="btn btn-sm" id="btnRegenerate"
style="background:#6a0dad;color:#fff;border:none;font-weight:500" onclick="regenerateFromDetail()">🔄 Sinh
lại</button>
<button class="btn btn-outline btn-sm" id="btnCopyJson" style="font-weight:500"
onclick="copyPopupText()">Copy Nội Dung</button>
<button class="btn btn-sm" id="btnSavePopupText"
style="background:#10b981;color:#fff;border:none;font-weight:500;" onclick="savePopupText()">Lưu
Update</button>
<button class="btn btn-outline btn-sm" style="font-weight:500" onclick="closeDetail()">Đóng</button>
</div>
</div>
<div style="display:flex; gap:24px; border-bottom:1px solid var(--border);">
<div class="tab-btn active" id="dTabClean" onclick="switchDetailTab('clean')"
style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid var(--primary); color:var(--primary);">
Dàn trang (Chuẩn)</div>
<div class="tab-btn" id="dTabEmbed" onclick="switchDetailTab('embed')"
style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid transparent; color:var(--muted-fg);">
Clean Descripts</div>
</div>
</div>
<!-- Fields Section --> <!-- Clean Tab -->
<div id="sectionFields" style="display:none;"> <div class="detail-body" id="detailBodyClean" style="max-height: 60vh; overflow-y: auto;">
<div style="display:flex; justify-content:space-between; margin-bottom:12px;"> <div class="table-loading">
<div> <div class="spinner-sm"></div> Đang tải...
<h2 style="margin:0; font-size:18px;">Cấu hình Prompt Fields</h2> </div>
<p style="margin:4px 0 0; font-size:13px; color:var(--muted-fg);">Các trường này sẽ được sinh động trong quá trình AI phân tích sản phẩm (Phase 1 & Phase 2).</p>
</div> </div>
<button class="btn btn-primary" onclick="showFieldForm()"><span style="margin-right:6px"></span> Thêm Trường</button>
</div>
<div style="border-radius:10px; overflow:hidden; border:1px solid var(--border); background:var(--card);">
<table class="product-table" id="fieldsTable">
<thead style="background:var(--muted);">
<tr>
<th style="width:15%">Key</th>
<th style="width:20%">Label</th>
<th style="width:40%">Instruction (Prompt)</th>
<th>Trạng thái</th>
<th style="width:120px">Thao tác</th>
</tr>
</thead>
<tbody id="fieldsTableBody">
<tr><td colspan="5" class="table-loading">Đang tải...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- INLINE EDIT POPUP --> <!-- Fields Tab -->
<div id="inlineEditPopup"> <div class="detail-body" id="detailBodyFields"
<div style="padding: 8px 10px; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px;">AI Editor (Codex)</div> style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card);">
<button class="inline-edit-btn" onclick="triggerInlineEdit('enrich')"><i class="fas fa-plus-circle" style="color: #10b981;"></i> Bổ sung gợi ý dịp mặc/lễ hội</button> </div>
<button class="inline-edit-btn" onclick="triggerInlineEdit('rewrite')"><i class="fas fa-pen" style="color: #3b82f6;"></i> Viết lại chuyên nghiệp hơn</button>
<button class="inline-edit-btn" onclick="triggerInlineEdit('shorten')"><i class="fas fa-compress-alt" style="color: #8b5cf6;"></i> Viết ngắn gọn lại</button>
<button class="inline-edit-btn" onclick="triggerInlineEdit('fix')"><i class="fas fa-wrench" style="color: #ef4444;"></i> Sửa lỗi chính tả</button>
</div>
<!-- Detail Overlay --> <!-- Embed Tab -->
<div class="detail-overlay" id="detailOverlay" onclick="if(event.target===this)closeDetail()"> <div class="detail-body" id="detailBodyEmbed"
<div class="detail-panel" style="width:800px;"> style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card); padding: 24px;">
<div class="detail-header" style="flex-direction:column; align-items:stretch; gap:12px; padding-bottom:0;"> <div id="detailPreEmbed"
<div style="display:flex; align-items:center; justify-content:space-between;"> style="font-size:14px; color:#374151; white-space:pre-wrap; line-height: 1.6; padding: 16px; background: #fefefe; border: 1px dashed #d1d5db; border-radius: 8px;">
<h2 id="detailTitle">Chi tiết mô tả</h2>
<div style="display:flex;gap:6px;">
<button class="btn btn-sm" id="btnApprove" style="background:#28a745;color:#fff;border:none;display:none;font-weight:500" onclick="approveDetail(1)">Duyệt</button>
<button class="btn btn-sm" id="btnReject" style="background:#dc3545;color:#fff;border:none;display:none;font-weight:500" onclick="approveDetail(0)">Hủy duyệt</button>
<button class="btn btn-sm" id="btnRegenerate" style="background:#6a0dad;color:#fff;border:none;font-weight:500" onclick="regenerateFromDetail()">🔄 Sinh lại</button>
<button class="btn btn-outline btn-sm" id="btnCopyJson" style="font-weight:500" onclick="copyPopupText()">Copy Nội Dung</button>
<button class="btn btn-sm" id="btnSavePopupText" style="background:#10b981;color:#fff;border:none;font-weight:500;" onclick="savePopupText()">Lưu Update</button>
<button class="btn btn-outline btn-sm" style="font-weight:500" onclick="closeDetail()">Đóng</button>
</div> </div>
</div> </div>
<div style="display:flex; gap:24px; border-bottom:1px solid var(--border);">
<div class="tab-btn active" id="dTabClean" onclick="switchDetailTab('clean')" style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid var(--primary); color:var(--primary);">Dàn trang (Chuẩn)</div> <!-- Raw Tab -->
<div class="tab-btn" id="dTabEmbed" onclick="switchDetailTab('embed')" style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid transparent; color:var(--muted-fg);">Clean Descripts</div> <div class="detail-body" id="detailBodyRaw"
style="display:none; background:#1e1e1e; color:#cfcbcc; max-height: 60vh; overflow-y: auto; padding: 20px;">
<pre id="detailPreRaw"
style="font-size:12px; font-family:var(--font-mono); margin:0; white-space:pre-wrap;"></pre>
</div> </div>
</div> </div>
</div>
<!-- Clean Tab -->
<div class="detail-body" id="detailBodyClean" style="max-height: 60vh; overflow-y: auto;">
<div class="table-loading"><div class="spinner-sm"></div> Đang tải...</div>
</div>
<!-- Fields Tab --> <!-- NATIVE BULK AI MODAL -->
<div class="detail-body" id="detailBodyFields" style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card);"> <div class="detail-overlay" id="nativeBulkAiModal" style="display:none;"
</div> onclick="if(event.target===this) this.style.display='none'">
<div class="detail-panel" style="width:600px; padding: 24px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 20px;">
<h2 style="margin:0; font-size:18px; color:var(--primary);">🤖 Bulk AI Edit — Trang Hiện Tại</h2>
<button class="btn btn-outline btn-sm"
onclick="document.getElementById('nativeBulkAiModal').style.display='none'"></button>
</div>
<div style="font-size:13px; color:var(--muted-fg); margin-bottom: 20px;">
Tính năng này sẽ gọi AI Codex gen lại TRƯỜNG DỮ LIỆU CỤ THỂ cho <strong id="lblBulkAiCount"
style="color:#0284c7;">0</strong>.
</div>
<!-- Embed Tab --> <div id="scopeContainer"></div>
<div class="detail-body" id="detailBodyEmbed" style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card); padding: 24px;">
<div id="detailPreEmbed" style="font-size:14px; color:#374151; white-space:pre-wrap; line-height: 1.6; padding: 16px; background: #fefefe; border: 1px dashed #d1d5db; border-radius: 8px;"></div>
</div>
<!-- Raw Tab --> <div style="margin-bottom:12px;">
<div class="detail-body" id="detailBodyRaw" style="display:none; background:#1e1e1e; color:#cfcbcc; max-height: 60vh; overflow-y: auto; padding: 20px;"> <label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">1. Chọn trường cần sửa / sinh
<pre id="detailPreRaw" style="font-size:12px; font-family:var(--font-mono); margin:0; white-space:pre-wrap;"></pre> thêm:</label>
</div> <select id="nativeBulkAiField" class="search-input" style="width:100%; max-width:100%;">
</div> <option value="huong_dan_size">Hướng dẫn chọn size</option>
</div> <option value="dip_mac">Dịp mặc</option>
<option value="phoi_do">Phối đồ</option>
<option value="mo_ta_chinh">Mô tả chính</option>
<option value="phong_cach">Phong cách</option>
<option value="ly_do_mua">Lý do mua</option>
<option value="luu_y_size">Lưu ý size</option>
<option value="layer">Gợi ý layering</option>
<option value="tags">Tags</option>
</select>
</div>
<!-- NATIVE BULK AI MODAL --> <div style="margin-bottom:12px;">
<div class="detail-overlay" id="nativeBulkAiModal" style="display:none;" onclick="if(event.target===this) this.style.display='none'"> <label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">2. Chế độ ghi đè:</label>
<div class="detail-panel" style="width:600px; padding: 24px;"> <select id="nativeBulkAiMode" class="search-input" style="width:100%; max-width:100%;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 20px;"> <option value="append">➕ Bổ sung thêm (Bảo tồn nội dung cũ)</option>
<h2 style="margin:0; font-size:18px; color:var(--primary);">🤖 Bulk AI Edit — Trang Hiện Tại</h2> <option value="overwrite">🔄 Viết lại hoàn toàn</option>
<button class="btn btn-outline btn-sm" onclick="document.getElementById('nativeBulkAiModal').style.display='none'"></button> </select>
</div> </div>
<div style="font-size:13px; color:var(--muted-fg); margin-bottom: 20px;">
Tính năng này sẽ gọi AI Codex gen lại TRƯỜNG DỮ LIỆU CỤ THỂ cho <strong id="lblBulkAiCount" style="color:#0284c7;">0</strong>.
</div>
<div id="scopeContainer"></div>
<div style="margin-bottom:12px;"> <div style="margin-bottom:20px;">
<label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">1. Chọn trường cần sửa / sinh thêm:</label> <label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">3. Hướng dẫn (Instruction /
<select id="nativeBulkAiField" class="search-input" style="width:100%; max-width:100%;"> Paste Bảng Size thô vào đây):</label>
<option value="huong_dan_size">Hướng dẫn chọn size</option> <textarea id="nativeBulkAiInstruction" class="search-input"
<option value="dip_mac">Dịp mặc</option> style="width:100%; max-width:100%; height:120px; resize:vertical;"
<option value="phoi_do">Phối đồ</option> placeholder="VD: Thêm rằng mẫu này mặc đi dự tiệc cuối năm rất đẹp... Hoặc paste Bảng Size Thô vào đây (S: 1m60-1m65, M: 1m65-1m70...)"></textarea>
<option value="mo_ta_chinh">Mô tả chính</option> </div>
<option value="phong_cach">Phong cách</option>
<option value="ly_do_mua">Lý do mua</option>
<option value="luu_y_size">Lưu ý size</option>
<option value="layer">Gợi ý layering</option>
<option value="tags">Tags</option>
</select>
</div>
<div style="margin-bottom:12px;"> <div style="display:flex; justify-content:flex-end; gap:10px;">
<label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">2. Chế độ ghi đè:</label> <button class="btn btn-outline"
<select id="nativeBulkAiMode" class="search-input" style="width:100%; max-width:100%;"> onclick="document.getElementById('nativeBulkAiModal').style.display='none'">Hủy</button>
<option value="append">➕ Bổ sung thêm (Bảo tồn nội dung cũ)</option> <button class="btn btn-primary" id="btnRunNativeBulkAi" onclick="execNativeBulkAi()">🚀 Chạy AI Ngay</button>
<option value="overwrite">🔄 Viết lại hoàn toàn</option> </div>
</select>
</div> </div>
</div>
<div style="margin-bottom:20px;"> <!-- Clean Data Section -->
<label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">3. Hướng dẫn (Instruction / Paste Bảng Size thô vào đây):</label> <div id="sectionCleanData"
<textarea id="nativeBulkAiInstruction" class="search-input" style="width:100%; max-width:100%; height:120px; resize:vertical;" placeholder="VD: Thêm rằng mẫu này mặc đi dự tiệc cuối năm rất đẹp... Hoặc paste Bảng Size Thô vào đây (S: 1m60-1m65, M: 1m65-1m70...)"></textarea> style="display:none; padding: 20px; background:var(--card); border:1px solid var(--border); border-radius:10px; min-height: 400px;">
<div style="display:flex; justify-content:space-between; margin-bottom:12px;">
<div>
<h2 style="margin:0; font-size:18px;">Chỉnh sửa Dữ liệu Sản phẩm (Clean Data)</h2>
<p style="margin:4px 0 0; font-size:13px; color:var(--muted-fg);">Khu vực chỉnh sửa, bổ sung, thêm/xóa trường dữ
liệu chuyên sâu cho 1 sản phẩm.</p>
</div>
<div>
<div style="display:inline-flex; border:1px solid var(--border); border-radius:6px; overflow:hidden;">
<input type="text" id="cleanDataSearch" placeholder="Nhập mã sản phẩm..."
style="border:none; padding:8px 12px; font-size:13px; outline:none; width:200px;">
<button class="btn btn-primary" style="border-radius:0; border:none;" onclick="loadCleanDataForInput()">Tải dữ
liệu</button>
</div>
<button class="btn btn-sm" id="btnSaveCleanData"
style="background:#0d6efd;color:#fff;border:none;display:none;margin-left:10px;padding:8px 16px;"
onclick="saveDetailEdit()">💾 Lưu Dữ liệu JSON</button>
</div>
</div> </div>
<div style="display:flex; justify-content:flex-end; gap:10px;"> <div id="cleanDataTitle"
<button class="btn btn-outline" onclick="document.getElementById('nativeBulkAiModal').style.display='none'">Hủy</button> style="font-weight:bold; font-size:16px; margin:20px 0 10px; color:var(--primary); display:none;"></div>
<button class="btn btn-primary" id="btnRunNativeBulkAi" onclick="execNativeBulkAi()">🚀 Chạy AI Ngay</button>
</div>
</div>
</div>
<!-- Clean Data Section --> <!-- Edit Tab that was previously in popup -->
<div id="sectionCleanData" style="display:none; padding: 20px; background:var(--card); border:1px solid var(--border); border-radius:10px; min-height: 400px;"> <div id="cleanDataEditor"
<div style="display:flex; justify-content:space-between; margin-bottom:12px;"> style="background:#fafafa; padding:20px; border:1px solid var(--border); border-radius:8px; display:none;">
<div> <!-- Rendered dynamically -->
<h2 style="margin:0; font-size:18px;">Chỉnh sửa Dữ liệu Sản phẩm (Clean Data)</h2>
<p style="margin:4px 0 0; font-size:13px; color:var(--muted-fg);">Khu vực chỉnh sửa, bổ sung, thêm/xóa trường dữ liệu chuyên sâu cho 1 sản phẩm.</p>
</div> </div>
<div>
<div style="display:inline-flex; border:1px solid var(--border); border-radius:6px; overflow:hidden;"> <div id="cleanDataEmpty"
<input type="text" id="cleanDataSearch" placeholder="Nhập mã sản phẩm..." style="border:none; padding:8px 12px; font-size:13px; outline:none; width:200px;"> style="padding:40px; text-align:center; color:var(--muted-fg); background:#f9fafb; border-radius:8px; border:2px dashed var(--border);">
<button class="btn btn-primary" style="border-radius:0; border:none;" onclick="loadCleanDataForInput()">Tải dữ liệu</button> Bạn chưa chọn sản phẩm nào để chỉnh sửa. Vui lòng nhập mã phía trên hoặc qua từ danh sách Sản phẩm.
</div>
<button class="btn btn-sm" id="btnSaveCleanData" style="background:#0d6efd;color:#fff;border:none;display:none;margin-left:10px;padding:8px 16px;" onclick="saveDetailEdit()">💾 Lưu Dữ liệu JSON</button>
</div> </div>
</div> </div>
<div id="cleanDataTitle" style="font-weight:bold; font-size:16px; margin:20px 0 10px; color:var(--primary); display:none;"></div>
<!-- Edit Tab that was previously in popup -->
<div id="cleanDataEditor" style="background:#fafafa; padding:20px; border:1px solid var(--border); border-radius:8px; display:none;">
<!-- Rendered dynamically -->
</div>
<div id="cleanDataEmpty" style="padding:40px; text-align:center; color:var(--muted-fg); background:#f9fafb; border-radius:8px; border:2px dashed var(--border);">
Bạn chưa chọn sản phẩm nào để chỉnh sửa. Vui lòng nhập mã phía trên hoặc qua từ danh sách Sản phẩm.
</div>
</div>
<script src="/static/product-desc/product-desc.js"></script> <script src="/static/product-desc/js/state.js?v=202604231530"></script>
<!-- Chatbot Widget --> <script src="/static/product-desc/js/core.js?v=202604231530"></script>
<script src="/static/product-desc/product-desc-chatbot.js?v=2"></script> <script src="/static/product-desc/js/table.js?v=202604231530"></script>
<script src="/static/product-desc/js/detail.js?v=202604231530"></script>
<script src="/static/product-desc/js/clean-data.js?v=202604231530"></script>
<script src="/static/product-desc/js/prompt-fields.js?v=202604231530"></script>
<script src="/static/product-desc/js/bulk-actions.js?v=202604231530"></script>
<script src="/static/product-desc/js/batch-jobs.js?v=202604231530"></script>
<script src="/static/product-desc/js/tags-manager.js?v=202604231530"></script>
<script src="/static/product-desc/js/size-guide.js?v=202604231530"></script>
<script src="/static/product-desc/js/inline-editor.js?v=202604231530"></script>
<!-- Chatbot Widget -->
<script src="/static/product-desc/product-desc-chatbot.js?v=2"></script>
</body> </body>
</html>
\ No newline at end of file </html>
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