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,10 +28,36 @@ class UltraDescriptionDB: ...@@ -28,10 +28,36 @@ 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()
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""" cur.execute(f"""
CREATE TABLE IF NOT EXISTS {TABLE} ( CREATE TABLE IF NOT EXISTS {TABLE} (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
...@@ -45,35 +71,18 @@ class UltraDescriptionDB: ...@@ -45,35 +71,18 @@ class UltraDescriptionDB:
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_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 status SMALLINT DEFAULT 0;
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS clean_description TEXT DEFAULT ''; 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 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 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 base_ref_code VARCHAR(50);
ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS ai_matches JSONB;
""") """)
# 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
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>
...@@ -18,15 +20,25 @@ ...@@ -18,15 +20,25 @@
<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>
<div style="display:flex;gap:8px;"> <div style="display:flex;gap:8px;">
<button class="btn btn-outline btn-sm" onclick="loadOverview();if(currentTab==='products') loadProducts(); else loadFields();">↻ Refresh</button> <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>
<!-- Tabs --> <!-- Tabs -->
<div style="display:flex; gap:20px; border-bottom:1px solid var(--border); margin-bottom:20px;"> <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 active" id="tabProducts" onclick="switchTab('products')"
<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> style="padding:10px 0; cursor:pointer; font-weight:600; font-size:14px; border-bottom:2px solid var(--primary); color:var(--primary);">
<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> 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 id="sectionProducts"> <div id="sectionProducts">
...@@ -58,7 +70,9 @@ ...@@ -58,7 +70,9 @@
<div class="stat-card"> <div class="stat-card">
<div class="label">Tiến độ duyệt</div> <div class="label">Tiến độ duyệt</div>
<div class="value" id="statProgress"></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 class="progress-bar-wrap">
<div class="progress-bar-fill" id="progressFill" style="width:0%"></div>
</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="label">Thiếu Clean Desc</div> <div class="label">Thiếu Clean Desc</div>
...@@ -146,17 +160,28 @@ ...@@ -146,17 +160,28 @@
<div class="filter-actions"> <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="loadProducts()">Lọc</button>
<button class="btn btn-outline-dark btn-sm" onclick="resetFilters()">Reset</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="btnApprovePage" onclick="batchApprovePage()">Duyệt (Trang
<button class="btn btn-dark btn-sm" id="btnApproveAll" style="font-weight:600;" onclick="approveAll()">Duyệt TOÀN BỘ</button> này)</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-dark btn-sm" id="btnApproveAll" style="font-weight:600;" onclick="approveAll()">Duyệt
<button class="btn btn-outline-dark btn-sm" id="btnBatch" onclick="batchGeneratePage()">Sinh AI (Trang này)</button> TOÀN BỘ</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-outline-dark btn-sm" id="btnBackfillClean" style="font-weight:600;"
<button class="btn btn-dark btn-sm" id="btnBatchTags" onclick="batchGenerateTags()">Bơm Tags (Toàn bộ)</button> onclick="backfillMissingCleanDescriptions()">Bù Clean Desc</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-outline-dark btn-sm" id="btnBatch" onclick="batchGeneratePage()">Sinh AI (Trang
<button class="btn btn-primary btn-sm" style="font-weight:bold; background:#0284c7; border-color:#0284c7;" onclick="openNativeBulkAiModal()">🤖 Bulk AI Edit (Custom)</button> 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>
</div><!-- Batch Tracking Bar --> </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 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="font-size:24px; filter: grayscale(1);">🤖</div>
<div style="flex:1;"> <div style="flex:1;">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;"> <div style="display:flex; justify-content:space-between; margin-bottom:6px;">
...@@ -166,12 +191,15 @@ ...@@ -166,12 +191,15 @@
<div style="background:rgba(255,255,255,0.7); height:8px; border-radius:4px; overflow:hidden;"> <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 id="batchTrackFill" style="background:#0ea5e9; height:100%; width:0%; transition:width 0.3s;"></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 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>
</div> </div>
<!-- Tags Batch Tracking Box --> <!-- 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 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="font-size:24px; filter: grayscale(1);">🏷️</div>
<div style="flex:1;"> <div style="flex:1;">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;"> <div style="display:flex; justify-content:space-between; margin-bottom:6px;">
...@@ -181,7 +209,37 @@ ...@@ -181,7 +209,37 @@
<div style="background:rgba(255,255,255,0.7); height:8px; border-radius:4px; overflow:hidden;"> <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 id="tagsTrackFill" style="background:#10b981; height:100%; width:0%; transition:width 0.3s;"></div>
</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 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>
<!-- 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>
...@@ -193,6 +251,8 @@ ...@@ -193,6 +251,8 @@
<th style="width:25%">Sản phẩm</th> <th style="width:25%">Sản phẩm</th>
<th>Dòng SP</th> <th>Dòng SP</th>
<th>Tags</th> <th>Tags</th>
<th>Giá</th>
<th>Giá</th> <th>Giá</th>
<th>Lượt bán</th> <th>Lượt bán</th>
<th>Trạng thái</th> <th>Trạng thái</th>
...@@ -203,7 +263,9 @@ ...@@ -203,7 +263,9 @@
</tr> </tr>
</thead> </thead>
<tbody id="tableBody"> <tbody id="tableBody">
<tr><td colspan="10" class="table-loading">Đang tải...</td></tr> <tr>
<td colspan="10" class="table-loading">Đang tải...</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
...@@ -218,9 +280,11 @@ ...@@ -218,9 +280,11 @@
<div style="display:flex; justify-content:space-between; margin-bottom:12px;"> <div style="display:flex; justify-content:space-between; margin-bottom:12px;">
<div> <div>
<h2 style="margin:0; font-size:18px;">Cấu hình Prompt Fields</h2> <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> <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> <button class="btn btn-primary" onclick="showFieldForm()"><span style="margin-right:6px"></span> Thêm
Trường</button>
</div> </div>
<div style="border-radius:10px; overflow:hidden; border:1px solid var(--border); background:var(--card);"> <div style="border-radius:10px; overflow:hidden; border:1px solid var(--border); background:var(--card);">
...@@ -235,79 +299,112 @@ ...@@ -235,79 +299,112 @@
</tr> </tr>
</thead> </thead>
<tbody id="fieldsTableBody"> <tbody id="fieldsTableBody">
<tr><td colspan="5" class="table-loading">Đang tải...</td></tr> <tr>
<td colspan="5" class="table-loading">Đang tải...</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
<!-- INLINE EDIT POPUP --> <!-- INLINE EDIT POPUP -->
<div id="inlineEditPopup"> <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> <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> style="padding: 8px 10px; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px;">
<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> AI Editor (Codex)</div>
<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('enrich')"><i class="fas fa-plus-circle"
<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> 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 --> <!-- Detail Overlay -->
<div class="detail-overlay" id="detailOverlay" onclick="if(event.target===this)closeDetail()"> <div class="detail-overlay" id="detailOverlay" onclick="if(event.target===this)closeDetail()">
<div class="detail-panel" style="width:800px;"> <div class="detail-panel" style="width:800px;">
<div class="detail-header" style="flex-direction:column; align-items:stretch; gap:12px; padding-bottom:0;"> <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;"> <div style="display:flex; align-items:center; justify-content:space-between;">
<h2 id="detailTitle">Chi tiết mô tả</h2> <h2 id="detailTitle">Chi tiết mô tả</h2>
<div style="display:flex;gap:6px;"> <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="btnApprove"
<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> style="background:#28a745;color:#fff;border:none;display:none;font-weight:500"
<button class="btn btn-sm" id="btnRegenerate" style="background:#6a0dad;color:#fff;border:none;font-weight:500" onclick="regenerateFromDetail()">🔄 Sinh lại</button> onclick="approveDetail(1)">Duyệt</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="btnReject"
<button class="btn btn-sm" id="btnSavePopupText" style="background:#10b981;color:#fff;border:none;font-weight:500;" onclick="savePopupText()">Lưu Update</button> 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> <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 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 active" id="dTabClean" onclick="switchDetailTab('clean')"
<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> 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>
</div> </div>
<!-- Clean Tab --> <!-- Clean Tab -->
<div class="detail-body" id="detailBodyClean" style="max-height: 60vh; overflow-y: auto;"> <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 class="table-loading">
<div class="spinner-sm"></div> Đang tải...
</div>
</div> </div>
<!-- Fields Tab --> <!-- Fields Tab -->
<div class="detail-body" id="detailBodyFields" style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card);"> <div class="detail-body" id="detailBodyFields"
style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card);">
</div> </div>
<!-- Embed Tab --> <!-- Embed Tab -->
<div class="detail-body" id="detailBodyEmbed" style="display:none; max-height: 60vh; overflow-y: auto; background: var(--card); padding: 24px;"> <div class="detail-body" id="detailBodyEmbed"
<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> 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>
<!-- Raw Tab --> <!-- Raw Tab -->
<div class="detail-body" id="detailBodyRaw" style="display:none; background:#1e1e1e; color:#cfcbcc; max-height: 60vh; overflow-y: auto; padding: 20px;"> <div class="detail-body" id="detailBodyRaw"
<pre id="detailPreRaw" style="font-size:12px; font-family:var(--font-mono); margin:0; white-space:pre-wrap;"></pre> 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>
</div>
<!-- NATIVE BULK AI MODAL --> <!-- 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-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 class="detail-panel" style="width:600px; padding: 24px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 20px;"> <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> <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> <button class="btn btn-outline btn-sm"
onclick="document.getElementById('nativeBulkAiModal').style.display='none'"></button>
</div> </div>
<div style="font-size:13px; color:var(--muted-fg); margin-bottom: 20px;"> <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>. 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>
<div id="scopeContainer"></div> <div id="scopeContainer"></div>
<div style="margin-bottom:12px;"> <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> <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%;"> <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="huong_dan_size">Hướng dẫn chọn size</option>
<option value="dip_mac">Dịp mặc</option> <option value="dip_mac">Dịp mặc</option>
...@@ -330,47 +427,71 @@ ...@@ -330,47 +427,71 @@
</div> </div>
<div style="margin-bottom:20px;"> <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> <label style="display:block; font-weight:600; font-size:13px; margin-bottom:4px;">3. Hướng dẫn (Instruction /
<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> 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>
<div style="display:flex; justify-content:flex-end; gap:10px;"> <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-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> <button class="btn btn-primary" id="btnRunNativeBulkAi" onclick="execNativeBulkAi()">🚀 Chạy AI Ngay</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Clean Data Section --> <!-- 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 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 style="display:flex; justify-content:space-between; margin-bottom:12px;">
<div> <div>
<h2 style="margin:0; font-size:18px;">Chỉnh sửa Dữ liệu Sản phẩm (Clean Data)</h2> <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> <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>
<div style="display:inline-flex; border:1px solid var(--border); border-radius:6px; overflow:hidden;"> <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;"> <input type="text" id="cleanDataSearch" placeholder="Nhập mã sản phẩm..."
<button class="btn btn-primary" style="border-radius:0; border:none;" onclick="loadCleanDataForInput()">Tải dữ liệu</button> 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> </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> <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> <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 --> <!-- 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;"> <div id="cleanDataEditor"
style="background:#fafafa; padding:20px; border:1px solid var(--border); border-radius:8px; display:none;">
<!-- Rendered dynamically --> <!-- Rendered dynamically -->
</div> </div>
<div id="cleanDataEmpty" style="padding:40px; text-align:center; color:var(--muted-fg); background:#f9fafb; border-radius:8px; border:2px dashed var(--border);"> <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. 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> </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> </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