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 = [
]
# Attempt to dynamically fetch product lines from DB to override the static list
try:
_conn = get_pooled_connection_compat()
_cur = _conn.cursor()
_cur.execute(f"SELECT DISTINCT product_line FROM {PG_TABLE} WHERE product_line IS NOT NULL AND product_line != ''")
_db_lines = [r[0] for r in _cur.fetchall()]
_cur.close()
_conn.close()
if _db_lines:
CANIFA_LINES = _db_lines
logger.info(f"✅ Loaded {len(CANIFA_LINES)} product lines dynamically from DB")
except Exception as e:
logger.warning(f"⚠️ Could not load product lines from DB, using fallback. Error: {e}")
# Removed dynamic loading on import to prevent blocking thread when DB is slow
# try:
# _conn = get_pooled_connection_compat()
# _cur = _conn.cursor()
# _cur.execute(f"SELECT DISTINCT product_line FROM {PG_TABLE} WHERE product_line IS NOT NULL AND product_line != ''")
# _db_lines = [r[0] for r in _cur.fetchall()]
# _cur.close()
# _conn.close()
# if _db_lines:
# CANIFA_LINES = _db_lines
# logger.info(f"✅ Loaded {len(CANIFA_LINES)} product lines dynamically from DB")
# 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.
......@@ -945,7 +946,7 @@ async def batch_generate_tags():
conn = get_pooled_connection_compat()
cur = conn.cursor()
# 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()
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:
"""Create ultra_descriptions table if it doesn't exist."""
if cls._initialized:
return
from config import USE_LOCAL_SQLITE
conn = None
try:
conn = get_pooled_connection_compat()
cur = conn.cursor()
cur.execute(f"""
CREATE TABLE IF NOT EXISTS {TABLE} (
id SERIAL PRIMARY KEY,
internal_ref_code VARCHAR(50) NOT NULL,
product_name VARCHAR(500),
product_image_url TEXT,
product_line VARCHAR(200),
description_data JSONB NOT NULL,
phase VARCHAR(20) DEFAULT 'enriched',
status SMALLINT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ultra_desc_ref_code
ON {TABLE}(internal_ref_code);
-- Migration: add columns if table already existed
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS status SMALLINT DEFAULT 0;
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS clean_description TEXT DEFAULT '';
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS tags JSONB DEFAULT '[]'::jsonb;
-- Migration v2: magento_ref_code support
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);
""")
# Drop old unique constraint on internal_ref_code (v2 migration: allow multiple colors per style)
try:
cur.execute(f"ALTER TABLE {TABLE} DROP CONSTRAINT IF EXISTS ultra_descriptions_internal_ref_code_key")
except Exception:
pass
# Non-unique index on internal_ref_code (for grouping)
try:
cur.execute(f"CREATE INDEX IF NOT EXISTS idx_ultra_desc_internal_code ON {TABLE}(internal_ref_code)")
except Exception:
pass
# Unique index on magento_ref_code (partial, ignores NULLs)
try:
cur.execute(f"""CREATE UNIQUE INDEX IF NOT EXISTS idx_ultra_desc_magento_code
ON {TABLE}(magento_ref_code) WHERE magento_ref_code IS NOT NULL;""")
except Exception:
pass # already exists
if USE_LOCAL_SQLITE:
# SQLite: Create full table at once with all columns
cur.execute(f"""
CREATE TABLE IF NOT EXISTS {TABLE} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
internal_ref_code TEXT NOT NULL,
magento_ref_code TEXT,
base_ref_code TEXT,
product_name TEXT,
product_image_url TEXT,
product_line TEXT,
description_data TEXT NOT NULL,
phase TEXT DEFAULT 'enriched',
status INTEGER DEFAULT 0,
clean_description TEXT DEFAULT '',
tags TEXT DEFAULT '[]',
ai_matches TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
else:
# PostgreSQL: Original logic
cur.execute(f"""
CREATE TABLE IF NOT EXISTS {TABLE} (
id SERIAL PRIMARY KEY,
internal_ref_code VARCHAR(50) NOT NULL,
product_name VARCHAR(500),
product_image_url TEXT,
product_line VARCHAR(200),
description_data JSONB NOT NULL,
phase VARCHAR(20) DEFAULT 'enriched',
status SMALLINT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS status SMALLINT DEFAULT 0;
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()
conn.commit()
cls._initialized = True
logger.info("✅ Table %s ready", TABLE)
logger.info("✅ Table %s ready (Mock: %s)", TABLE, USE_LOCAL_SQLITE)
except Exception as e:
logger.error("Error creating ultra_descriptions table: %s", e)
finally:
......@@ -134,9 +143,9 @@ class UltraDescriptionDB:
json.dumps(description_data, ensure_ascii=False), phase, clean_description),
)
row = cur.fetchone()
cur.close()
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)
return row_id
except Exception as e:
......@@ -565,7 +574,7 @@ class UltraDescriptionDB:
try:
conn = get_pooled_connection_compat()
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]
cur.close()
return count
......@@ -618,11 +627,11 @@ class UltraDescriptionDB:
conn.close()
# Auto-init table on import
try:
UltraDescriptionDB.ensure_table()
except Exception:
pass
# Removed auto-init on import to prevent blocking thread when DB is slow
# try:
# UltraDescriptionDB.ensure_table()
# except Exception:
# pass
# ═══════════════════════════════════════════════════════════════
......@@ -848,8 +857,8 @@ class DescFieldConfig:
conn.close()
# Auto-init field config table
try:
DescFieldConfig.ensure_table()
except Exception:
pass
# Removed auto-init on import to prevent blocking thread when DB is slow
# try:
# DescFieldConfig.ensure_table()
# except Exception:
# 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">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ultra Description — Canifa AI</title>
<link rel="stylesheet" href="/static/common/theme.css">
<link rel="stylesheet" href="/static/common/components.css">
<script src="/static/common/frame-detect.js"></script>
<link rel="stylesheet" href="/static/product-desc/product-desc.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ultra Description — Canifa AI</title>
<link rel="stylesheet" href="/static/common/theme.css">
<link rel="stylesheet" href="/static/common/components.css">
<script src="/static/common/frame-detect.js"></script>
<link rel="stylesheet" href="/static/product-desc/product-desc.css">
</head>
<body>
<div class="page">
<!-- Header -->
<div class="page-hdr" style="margin-bottom:10px;">
<div>
<h1>✦ Ultra Description Manager</h1>
<p>Sinh mô tả sản phẩm AI — kết hợp Vision + Stock Data</p>
<div class="page">
<!-- Header -->
<div class="page-hdr" style="margin-bottom:10px;">
<div>
<h1>✦ Ultra Description Manager</h1>
<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 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>
<!-- 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 id="sectionProducts">
<!-- Stats -->
<div class="stats-row" id="statsRow">
<div class="stat-card">
<div class="label">Tổng Magento Code</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">
<!-- Stats -->
<div class="stats-row" id="statsRow">
<div class="stat-card">
<div class="label">Tổng Magento Code</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>
<!-- Filters -->
<div class="filter-bar">
<input type="text" class="search-input" id="searchInput" placeholder="Tìm theo tên hoặc mã sản phẩm...">
<select id="statusFilter">
<option value="">Tất cả</option>
<option value="missing">✕ Chưa có mô tả</option>
<option value="pending">⏳ Chờ duyệt</option>
<option value="approved">✅ Đã duyệt</option>
<option value="has_desc">Đã generate (tất cả)</option>
</select>
<select id="lineFilter">
<option value="">Tất cả dòng SP</option>
</select>
<select id="tagsFilter" class="search-input" style="width: 220px;" onchange="loadProducts()">
<option value="">Lọc theo Tag DB (Tất cả)</option>
<optgroup label="Hoàn cảnh (Occasion)">
<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 (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 -->
<div class="filter-bar">
<input type="text" class="search-input" id="searchInput" placeholder="Tìm theo tên hoặc mã sản phẩm...">
<select id="statusFilter">
<option value="">Tất cả</option>
<option value="missing">✕ Chưa có mô tả</option>
<option value="pending">⏳ Chờ duyệt</option>
<option value="approved">✅ Đã duyệt</option>
<option value="has_desc">Đã generate (tất cả)</option>
</select>
<select id="lineFilter">
<option value="">Tất cả dòng SP</option>
</select>
<select id="tagsFilter" class="search-input" style="width: 220px;" onchange="loadProducts()">
<option value="">Lọc theo Tag DB (Tất cả)</option>
<optgroup label="Hoàn cảnh (Occasion)">
<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>
<!-- Tags Batch Tracking Box -->
<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);">
<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="tagsTrackTitle" style="color:#047857;">Hệ thống đang Auto-Tagging hàng loạt...</strong>
<span id="tagsTrackText" style="color:#059669; 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="tagsTrackFill" style="background:#10b981; height:100%; width:0%; transition:width 0.3s;"></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 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 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>
<!-- Tags Batch Tracking Box -->
<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);">
<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="tagsTrackTitle" style="color:#047857;">Hệ thống đang Auto-Tagging hàng loạt...</strong>
<span id="tagsTrackText" style="color:#059669; font-weight:600;">0 / 0</span>
<!-- Table -->
<div style="border-radius:10px; overflow:hidden; border:1px solid var(--border);">
<table class="product-table">
<thead>
<tr>
<th style="width:25%">Sản phẩm</th>
<th>Dòng SP</th>
<th>Tags</th>
<th>Giá</th>
<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 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 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>
<!-- Table -->
<div style="border-radius:10px; overflow:hidden; border:1px solid var(--border);">
<table class="product-table">
<thead>
<tr>
<th style="width:25%">Sản phẩm</th>
<th>Dòng SP</th>
<th>Tags</th>
<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>
<!-- INLINE EDIT POPUP -->
<div id="inlineEditPopup">
<div
style="padding: 8px 10px; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px;">
AI Editor (Codex)</div>
<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>
<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>
<!-- Pagination -->
<!-- Pagination -->
<div class="pagination" id="pagination"></div>
</div> <!-- end sectionProducts -->
<!-- Detail Overlay -->
<div class="detail-overlay" id="detailOverlay" onclick="if(event.target===this)closeDetail()">
<div class="detail-panel" style="width:800px;">
<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 -->
<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>
<!-- 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>
<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 -->
<div id="inlineEditPopup">
<div style="padding: 8px 10px; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px;">AI Editor (Codex)</div>
<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>
<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>
<!-- Fields Tab -->
<div class="detail-body" id="detailBodyFields"
style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card);">
</div>
<!-- Detail Overlay -->
<div class="detail-overlay" id="detailOverlay" onclick="if(event.target===this)closeDetail()">
<div class="detail-panel" style="width:800px;">
<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>
<!-- Embed Tab -->
<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>
<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>
<!-- Raw Tab -->
<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>
<!-- 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>
</div>
<!-- Fields Tab -->
<div class="detail-body" id="detailBodyFields" style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card);">
</div>
<!-- NATIVE BULK AI MODAL -->
<div class="detail-overlay" id="nativeBulkAiModal" style="display:none;"
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 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>
<div id="scopeContainer"></div>
<!-- Raw Tab -->
<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 style="margin-bottom:12px;">
<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>
<select id="nativeBulkAiField" class="search-input" style="width:100%; max-width:100%;">
<option value="huong_dan_size">Hướng dẫn chọn size</option>
<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 class="detail-overlay" id="nativeBulkAiModal" style="display:none;" 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>
<div id="scopeContainer"></div>
<div style="margin-bottom:12px;">
<label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">2. Chế độ ghi đè:</label>
<select id="nativeBulkAiMode" class="search-input" style="width:100%; max-width:100%;">
<option value="append">➕ Bổ sung thêm (Bảo tồn nội dung cũ)</option>
<option value="overwrite">🔄 Viết lại hoàn toàn</option>
</select>
</div>
<div style="margin-bottom:12px;">
<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>
<select id="nativeBulkAiField" class="search-input" style="width:100%; max-width:100%;">
<option value="huong_dan_size">Hướng dẫn chọn size</option>
<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>
<div style="margin-bottom:20px;">
<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>
<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>
</div>
<div style="margin-bottom:12px;">
<label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">2. Chế độ ghi đè:</label>
<select id="nativeBulkAiMode" class="search-input" style="width:100%; max-width:100%;">
<option value="append">➕ Bổ sung thêm (Bảo tồn nội dung cũ)</option>
<option value="overwrite">🔄 Viết lại hoàn toàn</option>
</select>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button class="btn btn-outline"
onclick="document.getElementById('nativeBulkAiModal').style.display='none'">Hủy</button>
<button class="btn btn-primary" id="btnRunNativeBulkAi" onclick="execNativeBulkAi()">🚀 Chạy AI Ngay</button>
</div>
</div>
</div>
<div style="margin-bottom:20px;">
<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>
<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>
<!-- Clean Data Section -->
<div id="sectionCleanData"
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 style="display:flex; justify-content:flex-end; gap:10px;">
<button class="btn btn-outline" onclick="document.getElementById('nativeBulkAiModal').style.display='none'">Hủy</button>
<button class="btn btn-primary" id="btnRunNativeBulkAi" onclick="execNativeBulkAi()">🚀 Chạy AI Ngay</button>
</div>
</div>
</div>
<div id="cleanDataTitle"
style="font-weight:bold; font-size:16px; margin:20px 0 10px; color:var(--primary); display:none;"></div>
<!-- Clean Data Section -->
<div id="sectionCleanData" 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>
<!-- 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>
<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 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>
<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>
<!-- Chatbot Widget -->
<script src="/static/product-desc/product-desc-chatbot.js?v=2"></script>
<script src="/static/product-desc/js/state.js?v=202604231530"></script>
<script src="/static/product-desc/js/core.js?v=202604231530"></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>
</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