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

feat(outfit): improve outfit recommendations with limited items per role and add pairing logic UI

- Reduce role_max_items from 20 to 2-3 to avoid duplicate items
- Simplify deduplicate logic to take top N by score
- Add outfit_pairing_data.json with 59 products and 250 pairings
- Add outfit-logic UI visualizer for fashion rules
- Fix main_router.py indentation for pairing_router
parent bd6f75f7
from fastapi import APIRouter from fastapi import APIRouter
from api.ai_report.report_html_route import router as report_html_router from api.ai_report.report_html_route import router as report_html_router
from api.api_sql.ai_sql_trace_route import router as ai_sql_trace_router from api.api_sql.ai_sql_trace_route import router as ai_sql_trace_router
...@@ -30,6 +30,7 @@ from api.experiment_log.experiment_log_route import router as experiment_log_rou ...@@ -30,6 +30,7 @@ from api.experiment_log.experiment_log_route import router as experiment_log_rou
from api.fashion_matches.outfit_matches_route import router as outfit_matches_router from api.fashion_matches.outfit_matches_route import router as outfit_matches_router
from api.fashion_matches.router import router as fashion_matches_router from api.fashion_matches.router import router as fashion_matches_router
from api.fashion_matches.simulator import router as fashion_matches_simulator_router from api.fashion_matches.simulator import router as fashion_matches_simulator_router
from api.outfit_match_rules.pairing_api import router as pairing_router
from api.feedback_agent.feedback_agent_route import router as feedback_agent_router from api.feedback_agent.feedback_agent_route import router as feedback_agent_router
# History & Auth # History & Auth
from api.history.check_history_route import router as check_history_router from api.history.check_history_route import router as check_history_router
...@@ -94,6 +95,7 @@ api_router.include_router(tags_direct_router) ...@@ -94,6 +95,7 @@ api_router.include_router(tags_direct_router)
api_router.include_router(fashion_matches_router) api_router.include_router(fashion_matches_router)
api_router.include_router(fashion_matches_simulator_router) api_router.include_router(fashion_matches_simulator_router)
api_router.include_router(outfit_matches_router) api_router.include_router(outfit_matches_router)
api_router.include_router(pairing_router)
api_router.include_router(ai_store_search_router) api_router.include_router(ai_store_search_router)
api_router.include_router(ai_image_search_router) api_router.include_router(ai_image_search_router)
......
{
"product_genders": {
"Blazer": ["men", "women", "boy"],
"Bộ mặc nhà": ["men", "girl", "boy", "women"],
"Bộ quần áo": ["boy", "girl", "men", "women", "unisex"],
"Bộ thể thao": ["men", "unisex", "boy"],
"Cardigan": ["girl", "women"],
"Chân váy": ["girl", "women"],
"Chăn cá nhân": ["others"],
"Găng tay chống nắng": ["unisex"],
"Khăn": ["unisex"],
"Khăn lau đầu": ["others"],
"Khăn mặt": ["others"],
"Khăn tắm": ["others"],
"Khẩu trang": ["unisex"],
"Mũ": ["unisex"],
"Mũ thể thao": ["unisex"],
"Pyjama": ["women", "girl", "boy"],
"Quần Body": ["men"],
"Quần Khaki": ["girl", "women", "men", "boy"],
"Quần culottes": ["unisex"],
"Quần dài": ["boy", "unisex", "men", "women", "girl"],
"Quần giữ nhiệt": ["women"],
"Quần jean": ["men", "girl", "boy", "women"],
"Quần leggings": ["women"],
"Quần leggings mặc nhà": ["girl"],
"Quần lót": ["women", "girl"],
"Quần lót tam giác": ["men", "boy"],
"Quần lót đùi": ["men", "boy"],
"Quần mặc nhà": ["unisex", "boy", "girl", "men", "women"],
"Quần nỉ": ["men", "girl", "unisex", "women", "boy"],
"Quần soóc": ["men", "girl", "boy", "women", "unisex"],
"Quần váy": ["girl", "women"],
"Túi xách": ["unisex"],
"Tất": ["women", "girl", "unisex", "boy", "men"],
"Váy liền": ["girl", "women"],
"Áo Body": ["unisex", "women", "girl", "men", "boy"],
"Áo Polo": ["boy", "unisex", "women", "girl", "men"],
"Áo Sơ mi": ["unisex", "men", "boy", "girl", "women"],
"Áo ba lỗ": ["men", "boy", "women", "unisex"],
"Áo giữ nhiệt": ["men", "boy", "women", "girl"],
"Áo hai dây": ["women"],
"Áo khoác": ["women", "men"],
"Áo khoác chần bông": ["men", "women", "boy", "unisex"],
"Áo khoác chống nắng": ["women", "men", "unisex"],
"Áo khoác dáng ngắn": ["men", "women", "unisex"],
"Áo khoác dạ": ["women"],
"Áo khoác gilet chần bông": ["women", "girl", "boy", "men"],
"Áo khoác gió": ["men", "boy", "women", "girl", "unisex"],
"Áo khoác lông vũ": ["men", "women"],
"Áo khoác nỉ có mũ": ["men", "women", "unisex"],
"Áo khoác nỉ không mũ": ["boy", "men", "women", "unisex", "girl"],
"Áo khoác sợi": ["women"],
"Áo kiểu": ["women", "girl"],
"Áo len": ["boy", "girl", "men", "women"],
"Áo len gilet": ["girl", "boy", "women"],
"Áo lót": ["girl"],
"Áo mặc nhà": ["men", "boy", "women", "girl"],
"Áo nỉ": ["men", "girl", "boy", "unisex", "women"],
"Áo nỉ có mũ": ["men", "girl", "unisex", "boy"],
"Áo phông": ["girl", "boy", "men", "unisex", "women"]
},
"pairings": [
{"anchor": "Blazer", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Blazer", "target": "Quần dài", "type": "b"},
{"anchor": "Blazer", "target": "Quần Khaki", "type": "b"},
{"anchor": "Blazer", "target": "Quần jean", "type": "b"},
{"anchor": "Blazer", "target": "Túi xách", "type": "a"},
{"anchor": "Bộ mặc nhà", "target": "Áo khoác", "type": "o"},
{"anchor": "Bộ mặc nhà", "target": "Khăn tắm", "type": "h"},
{"anchor": "Bộ mặc nhà", "target": "Khăn mặt", "type": "h"},
{"anchor": "Bộ mặc nhà", "target": "Chăn cá nhân", "type": "h"},
{"anchor": "Bộ quần áo", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Bộ quần áo", "target": "Mũ", "type": "a"},
{"anchor": "Bộ quần áo", "target": "Tất", "type": "a"},
{"anchor": "Bộ thể thao", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Bộ thể thao", "target": "Mũ thể thao", "type": "a"},
{"anchor": "Bộ thể thao", "target": "Tất", "type": "a"},
{"anchor": "Cardigan", "target": "Váy liền", "type": "t"},
{"anchor": "Cardigan", "target": "Áo phông", "type": "t"},
{"anchor": "Cardigan", "target": "Quần jean", "type": "b"},
{"anchor": "Cardigan", "target": "Chân váy", "type": "b"},
{"anchor": "Chân váy", "target": "Áo kiểu", "type": "t"},
{"anchor": "Chân váy", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Chân váy", "target": "Áo phông", "type": "t"},
{"anchor": "Chân váy", "target": "Áo len", "type": "t"},
{"anchor": "Chân váy", "target": "Cardigan", "type": "o"},
{"anchor": "Chân váy", "target": "Túi xách", "type": "a"},
{"anchor": "Chân váy", "target": "Quần lót", "type": "u"},
{"anchor": "Chăn cá nhân", "target": "Bộ mặc nhà", "type": "h"},
{"anchor": "Chăn cá nhân", "target": "Pyjama", "type": "h"},
{"anchor": "Găng tay chống nắng", "target": "Áo khoác chống nắng", "type": "o"},
{"anchor": "Găng tay chống nắng", "target": "Áo phông", "type": "t"},
{"anchor": "Găng tay chống nắng", "target": "Khẩu trang", "type": "a"},
{"anchor": "Khăn", "target": "Áo khoác", "type": "o"},
{"anchor": "Khăn", "target": "Áo len", "type": "t"},
{"anchor": "Khăn", "target": "Áo nỉ", "type": "t"},
{"anchor": "Khăn", "target": "Áo phông", "type": "t"},
{"anchor": "Khăn lau đầu", "target": "Bộ mặc nhà", "type": "h"},
{"anchor": "Khăn lau đầu", "target": "Pyjama", "type": "h"},
{"anchor": "Khăn mặt", "target": "Bộ mặc nhà", "type": "h"},
{"anchor": "Khăn mặt", "target": "Pyjama", "type": "h"},
{"anchor": "Khăn tắm", "target": "Bộ mặc nhà", "type": "h"},
{"anchor": "Khăn tắm", "target": "Pyjama", "type": "h"},
{"anchor": "Khẩu trang", "target": "Áo khoác chống nắng", "type": "o"},
{"anchor": "Khẩu trang", "target": "Áo phông", "type": "t"},
{"anchor": "Khẩu trang", "target": "Găng tay chống nắng", "type": "a"},
{"anchor": "Mũ", "target": "Áo phông", "type": "t"},
{"anchor": "Mũ", "target": "Bộ thể thao", "type": "t"},
{"anchor": "Mũ", "target": "Bộ quần áo", "type": "t"},
{"anchor": "Mũ", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Mũ", "target": "Áo Polo", "type": "t"},
{"anchor": "Mũ thể thao", "target": "Áo phông", "type": "t"},
{"anchor": "Mũ thể thao", "target": "Bộ thể thao", "type": "t"},
{"anchor": "Mũ thể thao", "target": "Áo nỉ", "type": "t"},
{"anchor": "Mũ thể thao", "target": "Tất", "type": "a"},
{"anchor": "Mũ thể thao", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Pyjama", "target": "Quần mặc nhà", "type": "b"},
{"anchor": "Pyjama", "target": "Khăn tắm", "type": "h"},
{"anchor": "Pyjama", "target": "Khăn mặt", "type": "h"},
{"anchor": "Quần Body", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Quần Body", "target": "Áo khoác", "type": "o"},
{"anchor": "Quần Body", "target": "Quần lót tam giác", "type": "u"},
{"anchor": "Quần Body", "target": "Quần lót đùi", "type": "u"},
{"anchor": "Quần Khaki", "target": "Áo Polo", "type": "t"},
{"anchor": "Quần Khaki", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Quần Khaki", "target": "Áo phông", "type": "t"},
{"anchor": "Quần Khaki", "target": "Blazer", "type": "o"},
{"anchor": "Quần Khaki", "target": "Túi xách", "type": "a"},
{"anchor": "Quần Khaki", "target": "Quần lót", "type": "u"},
{"anchor": "Quần Khaki", "target": "Quần lót tam giác", "type": "u"},
{"anchor": "Quần culottes", "target": "Áo phông", "type": "t"},
{"anchor": "Quần culottes", "target": "Áo Body", "type": "t"},
{"anchor": "Quần culottes", "target": "Túi xách", "type": "a"},
{"anchor": "Quần dài", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Quần dài", "target": "Áo Polo", "type": "t"},
{"anchor": "Quần dài", "target": "Áo len", "type": "t"},
{"anchor": "Quần dài", "target": "Blazer", "type": "o"},
{"anchor": "Quần dài", "target": "Áo khoác", "type": "o"},
{"anchor": "Quần dài", "target": "Tất", "type": "a"},
{"anchor": "Quần dài", "target": "Quần lót tam giác", "type": "u"},
{"anchor": "Quần dài", "target": "Quần lót đùi", "type": "u"},
{"anchor": "Quần giữ nhiệt", "target": "Áo giữ nhiệt", "type": "t"},
{"anchor": "Quần giữ nhiệt", "target": "Áo khoác chần bông", "type": "o"},
{"anchor": "Quần giữ nhiệt", "target": "Áo khoác lông vũ", "type": "o"},
{"anchor": "Quần jean", "target": "Áo phông", "type": "t"},
{"anchor": "Quần jean", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Quần jean", "target": "Áo nỉ", "type": "t"},
{"anchor": "Quần jean", "target": "Áo ba lỗ", "type": "t"},
{"anchor": "Quần jean", "target": "Áo len", "type": "t"},
{"anchor": "Quần jean", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Quần jean", "target": "Cardigan", "type": "o"},
{"anchor": "Quần jean", "target": "Tất", "type": "a"},
{"anchor": "Quần jean", "target": "Quần lót", "type": "u"},
{"anchor": "Quần jean", "target": "Quần lót tam giác", "type": "u"},
{"anchor": "Quần jean", "target": "Quần lót đùi", "type": "u"},
{"anchor": "Quần leggings", "target": "Áo phông", "type": "t"},
{"anchor": "Quần leggings", "target": "Áo len", "type": "t"},
{"anchor": "Quần leggings", "target": "Áo khoác dáng ngắn", "type": "o"},
{"anchor": "Quần leggings", "target": "Quần lót", "type": "u"},
{"anchor": "Quần leggings mặc nhà", "target": "Áo mặc nhà", "type": "t"},
{"anchor": "Quần leggings mặc nhà", "target": "Áo phông", "type": "t"},
{"anchor": "Quần lót", "target": "Quần jean", "type": "b"},
{"anchor": "Quần lót", "target": "Quần Khaki", "type": "b"},
{"anchor": "Quần lót", "target": "Chân váy", "type": "b"},
{"anchor": "Quần lót", "target": "Váy liền", "type": "b"},
{"anchor": "Quần lót", "target": "Quần leggings", "type": "b"},
{"anchor": "Quần lót tam giác", "target": "Quần jean", "type": "b"},
{"anchor": "Quần lót tam giác", "target": "Quần Khaki", "type": "b"},
{"anchor": "Quần lót tam giác", "target": "Quần dài", "type": "b"},
{"anchor": "Quần lót tam giác", "target": "Quần Body", "type": "b"},
{"anchor": "Quần lót tam giác", "target": "Quần soóc", "type": "b"},
{"anchor": "Quần lót đùi", "target": "Quần dài", "type": "b"},
{"anchor": "Quần lót đùi", "target": "Quần mặc nhà", "type": "b"},
{"anchor": "Quần lót đùi", "target": "Quần jean", "type": "b"},
{"anchor": "Quần lót đùi", "target": "Quần Body", "type": "b"},
{"anchor": "Quần lót đùi", "target": "Quần soóc", "type": "b"},
{"anchor": "Quần mặc nhà", "target": "Áo mặc nhà", "type": "t"},
{"anchor": "Quần mặc nhà", "target": "Áo phông", "type": "t"},
{"anchor": "Quần mặc nhà", "target": "Pyjama", "type": "h"},
{"anchor": "Quần nỉ", "target": "Áo nỉ", "type": "t"},
{"anchor": "Quần nỉ", "target": "Áo nỉ có mũ", "type": "t"},
{"anchor": "Quần nỉ", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Quần nỉ", "target": "Tất", "type": "a"},
{"anchor": "Quần nỉ", "target": "Bộ thể thao", "type": "t"},
{"anchor": "Quần soóc", "target": "Áo phông", "type": "t"},
{"anchor": "Quần soóc", "target": "Áo ba lỗ", "type": "t"},
{"anchor": "Quần soóc", "target": "Áo Polo", "type": "t"},
{"anchor": "Quần soóc", "target": "Mũ", "type": "a"},
{"anchor": "Quần soóc", "target": "Quần lót tam giác", "type": "u"},
{"anchor": "Quần soóc", "target": "Quần lót đùi", "type": "u"},
{"anchor": "Quần váy", "target": "Áo kiểu", "type": "t"},
{"anchor": "Quần váy", "target": "Áo phông", "type": "t"},
{"anchor": "Quần váy", "target": "Túi xách", "type": "a"},
{"anchor": "Quần váy", "target": "Quần lót", "type": "u"},
{"anchor": "Túi xách", "target": "Blazer", "type": "o"},
{"anchor": "Túi xách", "target": "Áo kiểu", "type": "t"},
{"anchor": "Túi xách", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Túi xách", "target": "Váy liền", "type": "t"},
{"anchor": "Túi xách", "target": "Áo khoác dạ", "type": "o"},
{"anchor": "Tất", "target": "Quần dài", "type": "b"},
{"anchor": "Tất", "target": "Quần jean", "type": "b"},
{"anchor": "Tất", "target": "Bộ thể thao", "type": "t"},
{"anchor": "Tất", "target": "Quần nỉ", "type": "b"},
{"anchor": "Váy liền", "target": "Cardigan", "type": "o"},
{"anchor": "Váy liền", "target": "Blazer", "type": "o"},
{"anchor": "Váy liền", "target": "Áo khoác dáng ngắn", "type": "o"},
{"anchor": "Váy liền", "target": "Túi xách", "type": "a"},
{"anchor": "Váy liền", "target": "Mũ", "type": "a"},
{"anchor": "Váy liền", "target": "Quần lót", "type": "u"},
{"anchor": "Áo Body", "target": "Quần jean", "type": "b"},
{"anchor": "Áo Body", "target": "Quần culottes", "type": "b"},
{"anchor": "Áo Body", "target": "Quần soóc", "type": "b"},
{"anchor": "Áo Body", "target": "Áo khoác dáng ngắn", "type": "o"},
{"anchor": "Áo Polo", "target": "Quần Khaki", "type": "b"},
{"anchor": "Áo Polo", "target": "Quần jean", "type": "b"},
{"anchor": "Áo Polo", "target": "Quần soóc", "type": "b"},
{"anchor": "Áo Polo", "target": "Mũ thể thao", "type": "a"},
{"anchor": "Áo Polo", "target": "Mũ", "type": "a"},
{"anchor": "Áo Sơ mi", "target": "Quần Khaki", "type": "b"},
{"anchor": "Áo Sơ mi", "target": "Quần dài", "type": "b"},
{"anchor": "Áo Sơ mi", "target": "Chân váy", "type": "b"},
{"anchor": "Áo Sơ mi", "target": "Blazer", "type": "o"},
{"anchor": "Áo Sơ mi", "target": "Áo len gilet", "type": "o"},
{"anchor": "Áo Sơ mi", "target": "Túi xách", "type": "a"},
{"anchor": "Áo ba lỗ", "target": "Quần soóc", "type": "b"},
{"anchor": "Áo ba lỗ", "target": "Quần jean", "type": "b"},
{"anchor": "Áo ba lỗ", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Áo giữ nhiệt", "target": "Áo khoác lông vũ", "type": "o"},
{"anchor": "Áo giữ nhiệt", "target": "Áo khoác chần bông", "type": "o"},
{"anchor": "Áo giữ nhiệt", "target": "Quần giữ nhiệt", "type": "b"},
{"anchor": "Áo hai dây", "target": "Áo khoác sợi", "type": "o"},
{"anchor": "Áo hai dây", "target": "Quần jean", "type": "b"},
{"anchor": "Áo hai dây", "target": "Chân váy", "type": "b"},
{"anchor": "Áo hai dây", "target": "Quần lót", "type": "u"},
{"anchor": "Áo khoác", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Áo khoác", "target": "Áo len", "type": "t"},
{"anchor": "Áo khoác", "target": "Quần dài", "type": "b"},
{"anchor": "Áo khoác", "target": "Khăn", "type": "a"},
{"anchor": "Áo khoác chần bông", "target": "Áo giữ nhiệt", "type": "t"},
{"anchor": "Áo khoác chần bông", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác chần bông", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo khoác chần bông", "target": "Quần jean", "type": "b"},
{"anchor": "Áo khoác chống nắng", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác chống nắng", "target": "Khẩu trang", "type": "a"},
{"anchor": "Áo khoác chống nắng", "target": "Găng tay chống nắng", "type": "a"},
{"anchor": "Áo khoác chống nắng", "target": "Quần jean", "type": "b"},
{"anchor": "Áo khoác dáng ngắn", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác dáng ngắn", "target": "Váy liền", "type": "t"},
{"anchor": "Áo khoác dáng ngắn", "target": "Quần jean", "type": "b"},
{"anchor": "Áo khoác dáng ngắn", "target": "Quần leggings", "type": "b"},
{"anchor": "Áo khoác dạ", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Áo khoác dạ", "target": "Áo len", "type": "t"},
{"anchor": "Áo khoác dạ", "target": "Quần dài", "type": "b"},
{"anchor": "Áo khoác dạ", "target": "Túi xách", "type": "a"},
{"anchor": "Áo khoác gilet chần bông", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác gilet chần bông", "target": "Áo nỉ", "type": "t"},
{"anchor": "Áo khoác gilet chần bông", "target": "Quần jean", "type": "b"},
{"anchor": "Áo khoác gilet chần bông", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo khoác gió", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác gió", "target": "Bộ thể thao", "type": "t"},
{"anchor": "Áo khoác gió", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo khoác gió", "target": "Mũ thể thao", "type": "a"},
{"anchor": "Áo khoác lông vũ", "target": "Áo giữ nhiệt", "type": "t"},
{"anchor": "Áo khoác lông vũ", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác lông vũ", "target": "Quần jean", "type": "b"},
{"anchor": "Áo khoác lông vũ", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo khoác nỉ có mũ", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác nỉ có mũ", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo khoác nỉ có mũ", "target": "Quần jean", "type": "b"},
{"anchor": "Áo khoác nỉ không mũ", "target": "Áo phông", "type": "t"},
{"anchor": "Áo khoác nỉ không mũ", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo khoác nỉ không mũ", "target": "Quần jean", "type": "b"},
{"anchor": "Áo khoác nỉ không mũ", "target": "Tất", "type": "a"},
{"anchor": "Áo khoác sợi", "target": "Áo hai dây", "type": "t"},
{"anchor": "Áo khoác sợi", "target": "Váy liền", "type": "t"},
{"anchor": "Áo khoác sợi", "target": "Quần jean", "type": "b"},
{"anchor": "Áo kiểu", "target": "Chân váy", "type": "b"},
{"anchor": "Áo kiểu", "target": "Quần váy", "type": "b"},
{"anchor": "Áo kiểu", "target": "Quần Khaki", "type": "b"},
{"anchor": "Áo kiểu", "target": "Blazer", "type": "o"},
{"anchor": "Áo kiểu", "target": "Túi xách", "type": "a"},
{"anchor": "Áo kiểu", "target": "Quần lót", "type": "u"},
{"anchor": "Áo len", "target": "Quần jean", "type": "b"},
{"anchor": "Áo len", "target": "Quần Khaki", "type": "b"},
{"anchor": "Áo len", "target": "Chân váy", "type": "b"},
{"anchor": "Áo len", "target": "Áo khoác", "type": "o"},
{"anchor": "Áo len", "target": "Khăn", "type": "a"},
{"anchor": "Áo len gilet", "target": "Áo Sơ mi", "type": "t"},
{"anchor": "Áo len gilet", "target": "Chân váy", "type": "b"},
{"anchor": "Áo len gilet", "target": "Quần dài", "type": "b"},
{"anchor": "Áo lót", "target": "Áo phông", "type": "t"},
{"anchor": "Áo lót", "target": "Áo kiểu", "type": "t"},
{"anchor": "Áo mặc nhà", "target": "Quần mặc nhà", "type": "b"},
{"anchor": "Áo mặc nhà", "target": "Quần leggings mặc nhà", "type": "b"},
{"anchor": "Áo nỉ", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo nỉ", "target": "Quần jean", "type": "b"},
{"anchor": "Áo nỉ", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Áo nỉ", "target": "Khăn", "type": "a"},
{"anchor": "Áo nỉ có mũ", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo nỉ có mũ", "target": "Quần jean", "type": "b"},
{"anchor": "Áo nỉ có mũ", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Áo phông", "target": "Quần jean", "type": "b"},
{"anchor": "Áo phông", "target": "Quần soóc", "type": "b"},
{"anchor": "Áo phông", "target": "Quần nỉ", "type": "b"},
{"anchor": "Áo phông", "target": "Chân váy", "type": "b"},
{"anchor": "Áo phông", "target": "Quần dài", "type": "b"},
{"anchor": "Áo phông", "target": "Áo khoác gió", "type": "o"},
{"anchor": "Áo phông", "target": "Cardigan", "type": "o"},
{"anchor": "Áo phông", "target": "Áo khoác dáng ngắn", "type": "o"},
{"anchor": "Áo phông", "target": "Mũ", "type": "a"},
{"anchor": "Áo phông", "target": "Tất", "type": "a"},
{"anchor": "Áo phông", "target": "Áo lót", "type": "u"}
],
"type_labels": {
"t": "top",
"b": "bottom",
"o": "outerwear",
"a": "accessory",
"u": "underwear",
"h": "home/other"
}
}
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Canifa AI Stylist — Outfit Logic Visualizer</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<style>
:root {
--bg: #0d0f14;
--bg2: #131720;
--bg3: #1c2130;
--border: #2a3148;
--text: #e8ecf4;
--muted: #7a8399;
--accent: #6c63ff;
--accent2: #00d4aa;
--warn: #ffb347;
--danger: #ff6b6b;
--neutral: #94a3b8;
--light-c: #f9a8d4;
--dark-c: #6366f1;
--card-r: 14px;
--trans: 0.2s ease;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* ── HEADER ─────────────────────────────── */
.header {
background: linear-gradient(135deg, #0d0f14 0%, #1a1f35 100%);
border-bottom: 1px solid var(--border);
padding: 20px 32px;
display: flex; align-items: center; justify-content: space-between;
}
.header-brand { display: flex; align-items: center; gap: 12px; }
.header-logo {
width: 38px; height: 38px; border-radius: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
display: flex; align-items: center; justify-content: center;
font-size: 18px;
}
.header-title { font-size: 17px; font-weight: 700; }
.header-sub { font-size: 12px; color: var(--muted); }
.version-badge {
font-size: 11px; font-weight: 600; padding: 4px 10px;
background: rgba(108,99,255,.2); border: 1px solid rgba(108,99,255,.4);
border-radius: 20px; color: var(--accent);
}
/* ── LAYOUT ─────────────────────────────── */
.main { display: grid; grid-template-columns: 280px 1fr; height: calc(100vh - 69px); }
/* ── SIDEBAR ─────────────────────────────── */
.sidebar {
background: var(--bg2);
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 20px 16px;
display: flex; flex-direction: column; gap: 6px;
}
.sidebar-title {
font-size: 11px; font-weight: 600; color: var(--muted);
text-transform: uppercase; letter-spacing: .08em; padding: 4px 8px 10px;
}
.nav-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border-radius: 9px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: var(--trans);
color: var(--muted); user-select: none;
}
.nav-item:hover { background: var(--bg3); color: var(--text); }
.nav-item.active {
background: linear-gradient(135deg, rgba(108,99,255,.2), rgba(0,212,170,.1));
border: 1px solid rgba(108,99,255,.3); color: var(--text);
}
.nav-item .icon { font-size: 16px; min-width: 20px; text-align: center; }
/* ── CONTENT ─────────────────────────────── */
.content { overflow-y: auto; padding: 28px 32px; }
.section { display: none; }
.section.active { display: block; }
.section-title {
font-size: 20px; font-weight: 700; margin-bottom: 6px;
}
.section-sub { font-size: 13px; color: var(--muted); margin-bottom: 24px; }
/* ── CARDS ─────────────────────────────── */
.card {
background: var(--bg2); border: 1px solid var(--border);
border-radius: var(--card-r); padding: 20px; margin-bottom: 16px;
}
.card-title {
font-size: 13px; font-weight: 600; color: var(--muted);
text-transform: uppercase; letter-spacing: .07em; margin-bottom: 14px;
}
/* ── COLOR PALETTE ─────────────────────────── */
.color-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 12px;
}
.color-chip {
border-radius: 10px; overflow: hidden;
border: 1px solid var(--border); cursor: pointer; transition: var(--trans);
}
.color-chip:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,.4); }
.color-swatch { height: 54px; }
.color-info { padding: 8px 10px; background: var(--bg3); }
.color-name { font-size: 12px; font-weight: 600; }
.color-group-tag {
display: inline-block; font-size: 10px; font-weight: 600;
padding: 2px 7px; border-radius: 10px; margin-top: 4px;
}
.tag-neutral { background: rgba(148,163,184,.2); color: var(--neutral); }
.tag-light { background: rgba(249,168,212,.2); color: var(--light-c); }
.tag-dark { background: rgba(99,102,241,.2); color: var(--dark-c); }
/* ── GROUP MATRIX ─────────────────────────── */
.matrix-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.matrix-table th, .matrix-table td {
padding: 10px 14px; border: 1px solid var(--border); text-align: center;
}
.matrix-table th { background: var(--bg3); font-weight: 600; }
.score-cell { font-weight: 700; border-radius: 4px; }
.score-high { background: rgba(0,212,170,.15); color: var(--accent2); }
.score-med { background: rgba(255,179,71,.12); color: var(--warn); }
.score-low { background: rgba(255,107,107,.12); color: var(--danger); }
/* ── CHECKER ─────────────────────────────── */
.checker-grid { display: grid; grid-template-columns: 1fr 40px 1fr; gap: 12px; align-items: center; }
.checker-arrow { text-align: center; font-size: 22px; color: var(--muted); }
label { font-size: 12px; font-weight: 600; color: var(--muted); display: block; margin-bottom: 6px; }
select, input[type=text] {
width: 100%; padding: 9px 12px;
background: var(--bg3); border: 1px solid var(--border);
border-radius: 8px; color: var(--text); font-size: 13px;
font-family: 'Inter', sans-serif; outline: none; transition: var(--trans);
}
select:focus, input[type=text]:focus { border-color: var(--accent); }
.btn {
padding: 9px 20px; border-radius: 8px; border: none; cursor: pointer;
font-size: 13px; font-weight: 600; font-family: 'Inter', sans-serif;
transition: var(--trans); display: inline-flex; align-items: center; gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), #5a51e8);
color: #fff;
}
.btn-primary:hover { opacity: .88; transform: translateY(-1px); }
.btn-secondary {
background: var(--bg3); color: var(--text); border: 1px solid var(--border);
}
.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
.result-box {
margin-top: 16px; padding: 16px; background: var(--bg3);
border-radius: 10px; border: 1px solid var(--border); min-height: 60px;
}
.result-advice {
font-size: 14px; font-weight: 500; line-height: 1.6; margin-bottom: 12px;
}
.score-bar-wrap { margin-top: 10px; }
.score-bar-label { font-size: 11px; color: var(--muted); margin-bottom: 4px; }
.score-bar {
height: 8px; background: var(--border); border-radius: 99px; overflow: hidden;
}
.score-bar-fill {
height: 100%; border-radius: 99px;
background: linear-gradient(90deg, var(--accent2), var(--accent));
transition: width .5s ease;
}
.group-pills { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.group-pill {
padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 600;
}
/* ── OUTFIT SIMULATOR ─────────────────────────── */
.sim-row { display: flex; gap: 12px; align-items: flex-end; margin-bottom: 16px; }
.sim-row .form-group { flex: 1; }
.outfit-result { display: none; }
.outfit-result.show { display: block; }
.product-hero {
display: flex; gap: 20px; align-items: flex-start;
background: var(--bg3); border: 1px solid var(--border);
border-radius: 12px; padding: 20px; margin-bottom: 20px;
}
.product-img {
width: 80px; height: 80px; border-radius: 10px;
object-fit: cover; background: var(--border);
display: flex; align-items: center; justify-content: center;
font-size: 28px; flex-shrink: 0;
}
.product-details { flex: 1; }
.product-name { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.product-meta { font-size: 12px; color: var(--muted); line-height: 1.8; }
.color-dot {
display: inline-block; width: 10px; height: 10px; border-radius: 50%;
margin-right: 5px; vertical-align: middle; border: 1px solid rgba(255,255,255,.2);
}
.strategy-box {
background: rgba(108,99,255,.08); border: 1px solid rgba(108,99,255,.2);
border-radius: 12px; padding: 16px; margin-bottom: 20px;
}
.strategy-summary { font-size: 14px; font-weight: 500; margin-bottom: 14px; }
.strategy-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
.strategy-card {
background: var(--bg2); border: 1px solid var(--border);
border-radius: 10px; padding: 12px;
}
.strategy-card-name { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
.strategy-card-desc { font-size: 11px; color: var(--muted); line-height: 1.5; margin-bottom: 8px; }
.strategy-colors { display: flex; flex-wrap: wrap; gap: 4px; }
.small-color-tag {
font-size: 10px; padding: 2px 8px; border-radius: 8px;
background: var(--bg3); border: 1px solid var(--border);
}
.occasion-tabs {
display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px;
}
.occ-tab {
padding: 7px 14px; border-radius: 20px; font-size: 12px; font-weight: 600;
cursor: pointer; border: 1px solid var(--border); background: var(--bg3);
color: var(--muted); transition: var(--trans);
}
.occ-tab:hover { border-color: var(--accent); color: var(--accent); }
.occ-tab.active {
background: rgba(108,99,255,.2); border-color: var(--accent); color: #fff;
}
.outfit-slots { display: grid; gap: 14px; }
.slot-section { }
.slot-label {
font-size: 11px; font-weight: 700; color: var(--muted);
text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px;
padding-bottom: 6px; border-bottom: 1px solid var(--border);
}
.outfit-items { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
.outfit-item {
background: var(--bg3); border: 1px solid var(--border);
border-radius: 10px; padding: 12px; transition: var(--trans);
}
.outfit-item:hover { border-color: var(--accent); }
.outfit-item-name { font-size: 12px; font-weight: 600; margin-bottom: 4px; line-height: 1.4; }
.outfit-item-color { font-size: 11px; color: var(--muted); margin-bottom: 6px; }
.outfit-item-score {
display: flex; align-items: center; gap: 6px; font-size: 11px;
}
.score-badge {
font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 6px;
}
.score-badge.high { background: rgba(0,212,170,.2); color: var(--accent2); }
.score-badge.med { background: rgba(255,179,71,.2); color: var(--warn); }
.score-badge.low { background: rgba(255,107,107,.2); color: var(--danger); }
.synergy-tip { font-size: 10px; color: var(--muted); margin-top: 5px; line-height: 1.4; }
/* ── LOADING / STATES ─────────────────────── */
.loading {
display: flex; align-items: center; justify-content: center;
gap: 10px; padding: 30px; color: var(--muted); font-size: 13px;
}
.spinner {
width: 18px; height: 18px; border-radius: 50%;
border: 2px solid var(--border); border-top-color: var(--accent);
animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state {
text-align: center; padding: 40px; color: var(--muted); font-size: 13px;
}
.empty-state .emoji { font-size: 32px; margin-bottom: 10px; }
/* ── STYLES GRID ─────────────────────────── */
.styles-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.style-pill {
padding: 7px 14px; border-radius: 20px; font-size: 12px; font-weight: 500;
background: var(--bg3); border: 1px solid var(--border);
cursor: default; transition: var(--trans);
}
.style-pill:hover { border-color: var(--accent2); color: var(--accent2); }
/* ── RESPONSIVE ─────────────────────────── */
@media (max-width: 768px) {
.main { grid-template-columns: 1fr; }
.sidebar { display: none; }
.content { padding: 16px; }
}
</style>
</head>
<body>
<header class="header">
<div class="header-brand">
<div class="header-logo">👗</div>
<div>
<div class="header-title">Canifa AI Stylist</div>
<div class="header-sub">Outfit Logic Visualizer</div>
</div>
</div>
<span class="version-badge" id="versionBadge">v—</span>
</header>
<div class="main">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-title">Menu</div>
<div class="nav-item active" data-section="colors" onclick="switchSection('colors')">
<span class="icon">🎨</span> Bảng Màu Sắc
</div>
<div class="nav-item" data-section="matrix" onclick="switchSection('matrix')">
<span class="icon">🔢</span> Ma Trận Nhóm Màu
</div>
<div class="nav-item" data-section="checker" onclick="switchSection('checker')">
<span class="icon"></span> Kiểm Tra Phối Màu
</div>
<div class="nav-item" data-section="outfit" onclick="switchSection('outfit')">
<span class="icon">🧍</span> Gợi Ý Outfit
</div>
<div class="nav-item" data-section="styles" onclick="switchSection('styles')">
<span class="icon"></span> Phong Cách
</div>
</nav>
<!-- Content -->
<main class="content">
<!-- ── SECTION: Colors ── -->
<section id="sec-colors" class="section active">
<div class="section-title">🎨 Bảng Màu Sắc</div>
<div class="section-sub">Toàn bộ 15 màu trong catalog với phân nhóm Neutral / Light / Dark</div>
<div id="colorGrid" class="color-grid">
<div class="loading"><div class="spinner"></div> Đang tải...</div>
</div>
</section>
<!-- ── SECTION: Matrix ── -->
<section id="sec-matrix" class="section">
<div class="section-title">🔢 Ma Trận Nhóm Màu</div>
<div class="section-sub">Điểm synergy (0–30) giữa các nhóm Neutral × Light × Dark</div>
<div class="card">
<div class="card-title">Color Group Synergy Matrix</div>
<div id="matrixTable"></div>
</div>
<div class="card" style="margin-top:0">
<div class="card-title">Giải Thích Công Thức</div>
<div style="display:grid;gap:12px">
<div style="paddig:12px;background:rgba(0,212,170,.06);border:1px solid rgba(0,212,170,.2);border-radius:10px;padding:14px">
<strong>🟢 Công thức "An Toàn" — Neutral + Neutral</strong>
<p style="font-size:13px;color:var(--muted);margin-top:6px">TOP Trắng/Be + BOTTOM Đen/Xám/Nâu. Phụ kiện cùng tông BOTTOM. Điểm: 30/30.</p>
</div>
<div style="background:rgba(249,168,212,.06);border:1px solid rgba(249,168,212,.2);border-radius:10px;padding:14px">
<strong>✨ Công thức "Điểm Nhấn" — Neutral + Light/Dark</strong>
<p style="font-size:13px;color:var(--muted);margin-top:6px">TOP màu nổi + BOTTOM Neutral (hoặc ngược lại). Phụ kiện Neutral để tiết chế. Điểm: 22–25/30.</p>
</div>
<div style="background:rgba(99,102,241,.06);border:1px solid rgba(99,102,241,.2);border-radius:10px;padding:14px">
<strong>🔥 Quy tắc 3 màu</strong>
<p style="font-size:13px;color:var(--muted);margin-top:6px">Không mặc quá 3 màu khác nhau. Nếu mặc mono-color, dùng phụ kiện kim loại Gold/Silver làm điểm nhấn.</p>
</div>
</div>
</div>
</section>
<!-- ── SECTION: Checker ── -->
<section id="sec-checker" class="section">
<div class="section-title">⚡ Kiểm Tra Phối Màu</div>
<div class="section-sub">Nhập 2 màu từ DB — AI phân tích nhóm màu và đưa ra lời khuyên ngay lập tức</div>
<div class="card">
<div class="checker-grid">
<div>
<label for="srcColor">Màu Sản Phẩm (TOP)</label>
<select id="srcColor"><option value="">-- Chọn màu --</option></select>
</div>
<div class="checker-arrow">+</div>
<div>
<label for="tgtColor">Màu Phối (BOTTOM)</label>
<select id="tgtColor"><option value="">-- Chọn màu --</option></select>
</div>
</div>
<div style="margin-top:16px">
<button class="btn btn-primary" onclick="checkColorLogic()">⚡ Kiểm Tra Ngay</button>
</div>
<div class="result-box" id="checkerResult">
<div class="empty-state"><div class="emoji">🎨</div>Chọn 2 màu và nhấn kiểm tra</div>
</div>
</div>
</section>
<!-- ── SECTION: Outfit Simulator ── -->
<section id="sec-outfit" class="section">
<div class="section-title">🧍 Gợi Ý Outfit</div>
<div class="section-sub">Nhập mã sản phẩm → AI trả về outfit hoàn chỉnh kèm giải thích màu sắc</div>
<div class="card">
<div class="sim-row">
<div class="form-group">
<label for="productCode">Mã Sản Phẩm (magento_ref_code)</label>
<input type="text" id="productCode" placeholder="ví dụ: 5TS25S021-SR079" />
</div>
<div class="form-group" style="max-width:180px">
<label for="occasionFilter">Dịp Mặc (tuỳ chọn)</label>
<select id="occasionFilter">
<option value="">Tất cả dịp</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadOutfit()" style="flex-shrink:0">🔍 Tìm Outfit</button>
</div>
</div>
<div class="outfit-result" id="outfitResult"></div>
</section>
<!-- ── SECTION: Styles ── -->
<section id="sec-styles" class="section">
<div class="section-title">✨ Phong Cách</div>
<div class="section-sub">Toàn bộ 16 phong cách được engine nhận diện từ DB</div>
<div class="card">
<div class="card-title">Danh Sách Style</div>
<div id="stylesGrid" class="styles-grid">
<div class="loading"><div class="spinner"></div> Đang tải...</div>
</div>
</div>
<div class="card">
<div class="card-title">Phối Style Theo Dịp</div>
<div id="styleOccMap" style="display:grid;gap:10px"></div>
</div>
</section>
</main>
</div>
<script>
const API = ''; // same origin
let META = null;
const COLOR_HEX = {
"Trắng":"#F5F5F5","Đen":"#1A1A1A","Be":"#D4B896","Xám":"#9E9E9E","Nâu":"#795548",
"Vàng":"#FDD835","Hồng":"#F48FB1","Xanh lam":"#64B5F6","Tím":"#BA68C8",
"Đỏ":"#EF5350","Cam":"#FFA726","Xanh lá":"#66BB6A",
"Xanh navy":"#1A237E","Xanh Jeans":"#5C6BC0","Xanh than":"#00897B"
};
// ── Nav ──────────────────────────────────────
function switchSection(id) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.querySelectorAll('[data-section="' + id + '"]').forEach(n => n.classList.add('active'));
}
// ── Load Meta ──────────────────────────────────────
async function loadMeta() {
try {
const r = await fetch(API + '/api/fashion-matches/rules/meta');
const d = await r.json();
if (!d.ok) throw new Error(d.error);
META = d.meta;
document.getElementById('versionBadge').textContent = 'v' + META.version;
renderColorGrid();
renderMatrix();
renderColorSelects();
renderOccasionFilter();
renderStyles();
} catch(e) {
console.error('Meta load error:', e);
}
}
// ── Color Grid ──────────────────────────────────────
function renderColorGrid() {
const grid = document.getElementById('colorGrid');
grid.innerHTML = '';
const groups = ['neutral','light','dark'];
const gLabel = {
neutral: 'Neutral (trung tính)',
light: 'Light (sáng / pastel)',
dark: 'Dark (đậm / nổi)'
};
META.colors.sort((a,b) => groups.indexOf(a.group)-groups.indexOf(b.group));
META.colors.forEach(c => {
const chip = document.createElement('div');
chip.className = 'color-chip';
const hex = COLOR_HEX[c.key] || c.hex || '#888';
chip.innerHTML = `
<div class="color-swatch" style="background:${hex}"></div>
<div class="color-info">
<div class="color-name">${c.key}</div>
<span class="color-group-tag tag-${c.group}">${gLabel[c.group] || c.group}</span>
</div>`;
grid.appendChild(chip);
});
}
// ── Matrix ──────────────────────────────────────
function renderMatrix() {
const m = META.color_group_matrix;
const groups = ['neutral','light','dark'];
const labels = {neutral:'⬜ Neutral',light:'🌸 Light',dark:'🔵 Dark'};
let html = '<table class="matrix-table"><thead><tr><th></th>';
groups.forEach(g => html += `<th>${labels[g]}</th>`);
html += '</tr></thead><tbody>';
groups.forEach(r => {
html += `<tr><th>${labels[r]}</th>`;
groups.forEach(c => {
const s = (m[r] || {})[c] || 0;
const cls = s >= 25 ? 'score-high' : s >= 15 ? 'score-med' : 'score-low';
html += `<td><div class="score-cell ${cls}">${s}/30</div></td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
document.getElementById('matrixTable').innerHTML = html;
}
// ── Color Selects ──────────────────────────────────────
function renderColorSelects() {
const dbColors = [
'Đen/ Black','Trắng/ White','Xanh da trời/ Blue','Xám/ Gray','Hồng/ Pink- Magenta',
'Be/ Beige','Đỏ/ Red','Tím/ Purple','Xanh lá cây/ Green','Màu xanh Jeans',
'Xanh than/ Aqua','Nâu/ Brown','Vàng/ Yellow + Gold','Cam/ Orange','Xám/ Grey'
];
['srcColor','tgtColor'].forEach(id => {
const sel = document.getElementById(id);
sel.innerHTML = '<option value="">-- Chọn màu --</option>';
dbColors.forEach(c => sel.innerHTML += `<option value="${c}">${c}</option>`);
});
}
// ── Occasion Filter ──────────────────────────────────────
function renderOccasionFilter() {
const sel = document.getElementById('occasionFilter');
META.occasions.forEach(o => {
sel.innerHTML += `<option value="${o.key}">${o.label}</option>`;
});
}
// ── Styles ──────────────────────────────────────
const OCC_STYLE_MAP = {
di_lam: ['Smart Casual','Formal','Minimalist','Basic','Basic Update','Feminine'],
di_tiec: ['Formal','Smart Casual','Minimalist','Feminine','Trend'],
the_thao: ['Sport','Casual','Streetwear','Athleisure','Dynamic'],
hang_ngay:['Casual','Basic','Streetwear','Minimalist','Essential','Basic Update'],
di_choi: ['Casual','Streetwear','Basic','Boho','Trend','Feminine','Dynamic'],
mac_nha: ['Casual','Basic','Essential']
};
function renderStyles() {
const g = document.getElementById('stylesGrid');
g.innerHTML = '';
if (META) {
META.styles.forEach(s => {
const p = document.createElement('div');
p.className = 'style-pill'; p.textContent = s; g.appendChild(p);
});
}
const om = document.getElementById('styleOccMap');
om.innerHTML = '';
(META ? META.occasions : []).forEach(o => {
const styles = OCC_STYLE_MAP[o.key] || [];
const row = document.createElement('div');
row.style.cssText = 'background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:12px';
row.innerHTML = `<div style="font-size:12px;font-weight:700;margin-bottom:8px">${o.label}</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">${styles.map(s=>`<span style="font-size:11px;padding:3px 10px;border-radius:12px;background:rgba(108,99,255,.15);border:1px solid rgba(108,99,255,.25);color:#a78df0">${s}</span>`).join('')}</div>`;
om.appendChild(row);
});
}
// ── Color Logic ──────────────────────────────────────
async function checkColorLogic() {
const src = document.getElementById('srcColor').value;
const tgt = document.getElementById('tgtColor').value;
const box = document.getElementById('checkerResult');
if (!src || !tgt) { box.innerHTML = '<div class="empty-state"><div class="emoji">⚠️</div>Vui lòng chọn cả 2 màu</div>'; return; }
box.innerHTML = '<div class="loading"><div class="spinner"></div> Đang phân tích...</div>';
try {
const r = await fetch(API + '/api/fashion-matches/color-logic', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({src_color: src, tgt_color: tgt})
});
const d = await r.json();
if (!d.ok) throw new Error(d.error);
const res = d.result;
const pct = Math.round((res.score / 30) * 100);
const gColors = {neutral:'var(--neutral)',light:'var(--light-c)',dark:'var(--dark-c)'};
box.innerHTML = `
<div class="result-advice">${res.advice}</div>
<div class="group-pills">
<span class="group-pill tag-${res.src_group}" style="color:${gColors[res.src_group]||'#fff'};background:rgba(0,0,0,.2)">${res.src_key || '?'}${res.src_group}</span>
<span style="color:var(--muted);align-self:center">+</span>
<span class="group-pill tag-${res.tgt_group}" style="color:${gColors[res.tgt_group]||'#fff'};background:rgba(0,0,0,.2)">${res.tgt_key || '?'}${res.tgt_group}</span>
</div>
<div class="score-bar-wrap" style="margin-top:14px">
<div class="score-bar-label">Điểm Synergy: <strong>${res.score} / ${res.max}</strong> (${pct}%)</div>
<div class="score-bar"><div class="score-bar-fill" style="width:${pct}%"></div></div>
</div>
<div style="font-size:11px;color:var(--muted);margin-top:8px">Phương pháp: ${res.method}</div>`;
} catch(e) {
box.innerHTML = `<div class="empty-state"><div class="emoji">❌</div>${e.message}</div>`;
}
}
// ── Outfit Simulator ──────────────────────────────────────
async function loadOutfit() {
const code = document.getElementById('productCode').value.trim();
const occ = document.getElementById('occasionFilter').value;
const box = document.getElementById('outfitResult');
if (!code) { alert('Vui lòng nhập mã sản phẩm'); return; }
box.className = 'outfit-result show';
box.innerHTML = '<div class="loading"><div class="spinner"></div> Đang tải outfit...</div>';
try {
const body = {code};
if (occ) body.occasion = occ;
const r = await fetch(API + '/api/fashion-matches/outfit-suggest', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const d = await r.json();
if (!d.ok) throw new Error(d.error);
renderOutfit(d);
} catch(e) {
box.innerHTML = `<div class="card"><div class="empty-state"><div class="emoji">❌</div>${e.message}</div></div>`;
}
}
function renderOutfit(d) {
const sp = d.source_product;
const cs = d.color_strategy;
const occ_data = d.outfit_by_occasion;
const gColors = {neutral:'var(--neutral)',light:'var(--light-c)',dark:'var(--dark-c)',undefined:'var(--muted)'};
const groupLabel = {
neutral: 'Neutral (trung tính)',
light: 'Light (sáng / pastel)',
dark: 'Dark (đậm / nổi)'
};
const hex = COLOR_HEX[sp.color_key] || '#888';
// Product hero
let html = `<div class="product-hero">
<div class="product-img" style="font-size:30px">👗</div>
<div class="product-details">
<div class="product-name">${sp.name || sp.code}</div>
<div class="product-meta">
<span class="color-dot" style="background:${hex}"></span>
<strong>${sp.color}</strong>
${sp.color_key ? ` → <span style="color:${gColors[sp.color_group]}">${sp.color_key} (${groupLabel[sp.color_group] || sp.color_group})</span>` : ''}
<br>📦 ${sp.product_line || '—'} &nbsp;|&nbsp; 🧑 ${sp.gender || '—'}
${sp.style ? `<br>✨ ${sp.style}` : ''}
${sp.material ? `<br>🧵 ${sp.material}` : ''}
</div>
</div>
</div>`;
// Color strategy
html += `<div class="strategy-box">
<div class="strategy-summary">🎨 ${cs.summary}</div>
<div class="strategy-cards">`;
cs.strategies.forEach(s => {
html += `<div class="strategy-card">
<div class="strategy-card-name">${s.name}</div>
<div class="strategy-card-desc">${s.desc}</div>
${s.score !== null && s.score !== undefined ? `<div style="font-size:10px;color:var(--muted)">Điểm: ${s.score}/30</div>` : ''}
<div class="strategy-colors">
${(s.example_colors||s.example_bottoms||[]).slice(0,5).map(c => {
const ch = COLOR_HEX[c] || '#888';
return `<span class="small-color-tag"><span class="color-dot" style="background:${ch}"></span>${c}</span>`;
}).join('')}
</div>
</div>`;
});
html += `</div></div>`;
// Occasion tabs + slots
const occs = Object.keys(occ_data);
if (occs.length === 0) {
html += `<div class="card"><div class="empty-state"><div class="emoji">🤷</div>Chưa có ai_matches — hãy chạy Stylist Engine trước</div></div>`;
} else {
html += `<div class="occasion-tabs" id="occTabs">`;
occs.forEach((o, i) => {
html += `<div class="occ-tab${i===0?' active':''}" onclick="switchOcc('${o}')" data-occ="${o}">${occ_data[o].label}</div>`;
});
html += `</div>`;
occs.forEach((o, i) => {
const slots = occ_data[o].slots;
html += `<div class="outfit-slots" id="occ-${o}" style="display:${i===0?'grid':'none'}">`;
const roleLabel = {bottom:'👖 Quần / Chân Váy',outerwear:'🧥 Áo Khoác / Layer Ngoài',accessory:'👜 Phụ Kiện'};
Object.entries(slots).forEach(([role, items]) => {
html += `<div class="slot-section">
<div class="slot-label">${roleLabel[role] || role}</div>
<div class="outfit-items">`;
items.forEach(item => {
const ih = COLOR_HEX[item.color_key] || '#888';
const syn = item.color_synergy || {};
const score = item.score || 0;
const badgeCls = score >= 70 ? 'high' : score >= 50 ? 'med' : 'low';
html += `<div class="outfit-item">
<div class="outfit-item-name">${item.name || item.code}</div>
<div class="outfit-item-color">
<span class="color-dot" style="background:${ih}"></span>
${item.color || '—'}
${item.color_group && item.color_group !== '?' ? `<span style="font-size:10px;color:${gColors[item.color_group]}"> (${groupLabel[item.color_group] || item.color_group})</span>` : ''}
</div>
<div class="outfit-item-score">
<span class="score-badge ${badgeCls}">${score}</span>
<span style="color:var(--muted)">điểm phù hợp</span>
</div>
${syn.advice ? `<div class="synergy-tip">${syn.advice}</div>` : ''}
</div>`;
});
html += `</div></div>`;
});
html += `</div>`;
});
}
document.getElementById('outfitResult').innerHTML = html;
}
function switchOcc(id) {
document.querySelectorAll('.occ-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('[data-occ]').forEach(t => { if(t.dataset.occ===id) t.classList.add('active'); });
document.querySelectorAll('[id^="occ-"]').forEach(el => el.style.display = 'none');
const el = document.getElementById('occ-' + id);
if (el) el.style.display = 'grid';
}
// ── Init ──────────────────────────────────────
loadMeta();
</script>
</body>
</html>
<h2 class="sr-only">Bảng logic phối đồ Canifa – 59 sản phẩm theo giới tính</h2>
<style>
.tg{display:inline-block;font-size:11px;padding:2px 7px;border-radius:4px;margin:1px 2px;white-space:nowrap;font-weight:500;line-height:1.6}
.t-t{background:#E1F5EE;color:#085041}.t-b{background:#E6F1FB;color:#0C447C}
.t-o{background:#EEEDFE;color:#3C3489}.t-a{background:#FAEEDA;color:#633806}
.t-u{background:#FBEAF0;color:#72243E}.t-h{background:#F1EFE8;color:#444441}
.gb{display:inline-block;font-size:10px;padding:1px 5px;border-radius:3px;margin:1px;white-space:nowrap;font-weight:500}
.gm{background:#E6F1FB;color:#0C447C}.gw{background:#FBEAF0;color:#72243E}
.gbo{background:#E1F5EE;color:#085041}.gg{background:#FAEEDA;color:#633806}
.gu{background:#EEEDFE;color:#3C3489}.go{background:#F1EFE8;color:#444441}
.fb{padding:5px 12px;border-radius:var(--border-radius-md);border:0.5px solid var(--color-border-secondary);background:transparent;cursor:pointer;font-size:12px;color:var(--color-text-secondary);white-space:nowrap}
.fb.on{background:var(--color-background-info);color:var(--color-text-info);border-color:transparent}
#tbl td,#tbl th{padding:7px 10px;vertical-align:top;font-size:13px;border-bottom:0.5px solid var(--color-border-tertiary)}
#tbl th{font-size:11px;font-weight:500;color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.03em;border-bottom:0.5px solid var(--color-border-primary)}
#tbl tr:hover td{background:var(--color-background-secondary)}
</style>
<div style="padding:.75rem 0">
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;align-items:center">
<span style="font-size:12px;color:var(--color-text-secondary);margin-right:4px">Loại tag:</span>
<span class="tg t-t">Top (áo trong)</span><span class="tg t-b">Bottom (quần/váy)</span>
<span class="tg t-o">Khoác ngoài</span><span class="tg t-a">Phụ kiện</span>
<span class="tg t-u">Đồ lót</span><span class="tg t-h">Nhà/khác</span>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px">
<button class="fb on" onclick="sg(this,'all')">Tất cả</button>
<button class="fb" onclick="sg(this,'men')">Nam (men)</button>
<button class="fb" onclick="sg(this,'women')">Nữ (women)</button>
<button class="fb" onclick="sg(this,'boy')">Bé trai (boy)</button>
<button class="fb" onclick="sg(this,'girl')">Bé gái (girl)</button>
<button class="fb" onclick="sg(this,'unisex')">Unisex</button>
</div>
<input type="text" id="q" placeholder="Tìm sản phẩm..." oninput="rd()" style="width:100%;box-sizing:border-box;margin-bottom:8px">
<div id="st" style="font-size:12px;color:var(--color-text-secondary);margin-bottom:8px"></div>
<div style="overflow-x:auto;border:0.5px solid var(--color-border-tertiary);border-radius:var(--border-radius-lg)">
<table id="tbl" style="width:100%;border-collapse:collapse">
<thead><tr>
<th style="min-width:130px">Sản phẩm</th>
<th style="min-width:130px">Giới tính</th>
<th>Phối với</th>
</tr></thead>
<tbody id="tb"></tbody>
</table>
</div></div>
<script>
let PG = {};
let D = [];
let TI = {t:{c:'t-t',l:'top'},b:{c:'t-b',l:'bottom'},o:{c:'t-o',l:'khoác'},a:{c:'t-a',l:'phụ kiện'},u:{c:'t-u',l:'đồ lót'},h:{c:'t-h',l:'nhà/khác'}};
let GCL = {men:'gm',women:'gw',boy:'gbo',girl:'gg',unisex:'gu',others:'go'};
let cg = 'all';
// Fetch data from API
async function loadData() {
try {
const res = await fetch('/api/outfit-match-rules/pairings');
const data = await res.json();
if (!data.success) throw new Error(data.error || 'Failed to load');
// Get product genders
const gendersRes = await fetch('/api/outfit-match-rules/products');
const gendersData = await gendersRes.json();
PG = gendersData.product_genders || {};
// Transform pairings to D format
D = [];
const pairingsByAnchor = {};
data.pairings.forEach(p => {
if (!pairingsByAnchor[p.anchor]) {
pairingsByAnchor[p.anchor] = [];
}
pairingsByAnchor[p.anchor].push([p.target, p.type]);
});
// Convert to array format matching original D structure
Object.keys(pairingsByAnchor).sort().forEach(anchor => {
D.push({ n: anchor, p: pairingsByAnchor[anchor] });
});
rd();
} catch(e) {
console.error('Error loading data:', e);
document.getElementById('tb').innerHTML = '<tr><td colspan="3" style="text-align:center;color:red;padding:2rem">Lỗi tải dữ liệu</td></tr>';
}
}
function cs(a,t,g){
if(g==='all')return true;
const aG=PG[a]||[];
if(!aG.includes(g))return false;
const tG=PG[t]||['others'];
if(g==='unisex')return tG.some(x=>['men','women','unisex','others'].includes(x));
return tG.includes(g)||tG.includes('unisex')||tG.includes('others');
}
function rd(){
const q=document.getElementById('q').value.toLowerCase();
let rows='',shown=0;
D.forEach(item=>{
const aG=PG[item.n]||['others'];
if(cg!=='all'&&!aG.includes(cg))return;
if(q&&!item.n.toLowerCase().includes(q))return;
const pairs=item.p.filter(([t])=>cs(item.n,t,cg));
if(!pairs.length&&cg!=='all')return;
shown++;
const gb=aG.map(g=>`<span class="gb ${GCL[g]||'go'}">${g}</span>`).join('');
const pt=pairs.map(([t,tp])=>{const i=TI[tp]||TI.h;return`<span class="tg ${i.c}">${t}</span>`;}).join('');
rows+=`<tr><td style="font-weight:500;color:var(--color-text-primary);white-space:nowrap">${item.n}</td><td>${gb}</td><td>${pt}</td></tr>`;
});
document.getElementById('tb').innerHTML=rows||'<tr><td colspan="3" style="text-align:center;color:var(--color-text-secondary);padding:2rem">Không tìm thấy</td></tr>';
document.getElementById('st').textContent=`Hiển thị ${shown} sản phẩm`;
}
function sg(btn,g){
cg=g;
document.querySelectorAll('.fb').forEach(b=>b.classList.remove('on'));
btn.classList.add('on');
rd();
}
rd();
</script>
...@@ -86,10 +86,11 @@ ...@@ -86,10 +86,11 @@
"accessory": 4 "accessory": 4
}, },
"role_max_items": { "role_max_items": {
"top": 20, "top": 3,
"bottom": 20, "bottom": 3,
"outerwear": 20, "outerwear": 2,
"accessory": 20 "accessory": 2,
"underwear": 2
}, },
"_comment_product_line_to_role": "Ánh xạ tên product_line_vn → role trong outfit", "_comment_product_line_to_role": "Ánh xạ tên product_line_vn → role trong outfit",
"product_line_to_role": { "product_line_to_role": {
......
...@@ -286,7 +286,7 @@ class StylistEngine: ...@@ -286,7 +286,7 @@ class StylistEngine:
target_gender = SQLITE_GENDER_MAP.get(gender_key, "unisex") target_gender = SQLITE_GENDER_MAP.get(gender_key, "unisex")
cur.execute( cur.execute(
f"""SELECT id, occasion_tag as occasion, match_role, target_category, ai_reason f"""SELECT id, occasion as occasion, match_role, target_category, ai_reason
FROM {TABLE_OUTFIT_RULES} FROM {TABLE_OUTFIT_RULES}
WHERE UPPER(anchor_category) = UPPER(?) WHERE UPPER(anchor_category) = UPPER(?)
AND (gender_target = ? OR gender_target = 'unisex' OR gender_target = 'all')""", AND (gender_target = ? OR gender_target = 'unisex' OR gender_target = 'all')""",
...@@ -517,7 +517,7 @@ class StylistEngine: ...@@ -517,7 +517,7 @@ class StylistEngine:
# 3. Occasion score (0-20) - Reactivated to use NLP tags # 3. Occasion score (0-20) - Reactivated to use NLP tags
if "occasion" in weights: if "occasion" in weights:
occ_score = self._occasion_score(src.get("occasion_tags", []), tgt.get("occasion_tags", []), occasion) occ_score = self._occasion_score(src.get("occasion", []), tgt.get("occasion", []), occasion)
total += int(occ_score * weights["occasion"] / 20) total += int(occ_score * weights["occasion"] / 20)
# 4. Role bonus — Bottom gets priority over accessories (0-15) # 4. Role bonus — Bottom gets priority over accessories (0-15)
...@@ -747,25 +747,8 @@ class StylistEngine: ...@@ -747,25 +747,8 @@ class StylistEngine:
def _deduplicate(self, items: list[dict], max_n: int) -> list[dict]: def _deduplicate(self, items: list[dict], max_n: int) -> list[dict]:
"""Remove near-duplicates logic (Enforce product diversity + color diversity).""" """Take top N items by score (already sorted). Simple and effective."""
seen_lines: set[str] = set() return items[:max_n]
seen_colors: set[str] = set()
result = []
for item in items:
pl = item.get("product_line", item["code"])
# Canonicalize color slightly for filtering
color_val = self._detect_color_key(item.get("color", ""), self.rules["color_keys"]) or item.get("color", "")
# Prioritize inserting if we haven't seen this product line OR color yet
if pl not in seen_lines or color_val not in seen_colors or len(result) < max_n // 2:
result.append(item)
seen_lines.add(pl)
if color_val:
seen_colors.add(color_val)
if len(result) >= max_n:
break
return result
# ────────────────────────────────────────── # ──────────────────────────────────────────
# Data Loading # Data Loading
...@@ -811,7 +794,7 @@ class StylistEngine: ...@@ -811,7 +794,7 @@ class StylistEngine:
"age_group": row["age_group"] or "", "age_group": row["age_group"] or "",
"image": row["image"] or "", "image": row["image"] or "",
"style_tags": [], "style_tags": [],
"occasion_tags": [], "occasion": [],
"material_tags": [], "material_tags": [],
"season_tags": [], "season_tags": [],
}) })
...@@ -874,7 +857,7 @@ class StylistEngine: ...@@ -874,7 +857,7 @@ class StylistEngine:
item["role"], # match_role item["role"], # match_role
item["score"], item["score"],
item["reason"], item["reason"],
item["occasion"], # occasion (NOT occasion_tag) item["occasion"],
)) ))
if rows: if rows:
...@@ -915,7 +898,7 @@ class StylistEngine: ...@@ -915,7 +898,7 @@ class StylistEngine:
occ_pts = 0 occ_pts = 0
raw_occ = 0 raw_occ = 0
if "occasion" in weights: if "occasion" in weights:
raw_occ = self._occasion_score(src.get("occasion_tags", []), tgt.get("occasion_tags", []), occ_target) raw_occ = self._occasion_score(src.get("occasion", []), tgt.get("occasion", []), occ_target)
occ_pts = int(raw_occ * weights["occasion"] / 20) occ_pts = int(raw_occ * weights["occasion"] / 20)
# Role # Role
......
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