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

feat: canifa ai platform cookbook — full documentation site

parent b854086e
/* ═══════════════════════════════════════════════════════════
Cookbook — Premium Dark Theme
═══════════════════════════════════════════════════════════ */
:root {
--cb-bg: #0f172a;
--cb-sidebar-bg: #1e293b;
--cb-card-bg: #1e293b;
--cb-text: #f1f5f9;
--cb-text-muted: #94a3b8;
--cb-accent: #3b82f6;
--cb-border: #334155;
--cb-code-bg: #1a1a1a;
}
body.light-theme {
--cb-bg: #f8fafc;
--cb-sidebar-bg: #ffffff;
--cb-card-bg: #ffffff;
--cb-text: #1e293b;
--cb-text-muted: #64748b;
--cb-accent: #2563eb;
--cb-border: #e2e8f0;
--cb-code-bg: #f1f5f9;
}
body {
background: var(--cb-bg);
color: var(--cb-text);
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
transition: background 0.3s, color 0.3s;
}
.cookbook-container {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.cookbook-sidebar {
width: 300px;
background: var(--cb-sidebar-bg);
border-right: 1px solid var(--cb-border);
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
z-index: 100;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--cb-border);
}
.search-container {
position: relative;
display: flex;
align-items: center;
}
#recipeSearch {
width: 100%;
background: var(--cb-bg);
border: 1px solid var(--cb-border);
border-radius: 8px;
padding: 8px 32px 8px 12px;
color: var(--cb-text);
font-size: 14px;
}
.search-icon {
position: absolute;
right: 10px;
color: var(--cb-text-muted);
}
.recipe-nav {
flex: 1;
overflow-y: auto;
padding: 10px 0;
}
.nav-group {
margin-bottom: 10px;
}
.nav-group-title {
padding: 10px 20px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
color: var(--cb-text-muted);
letter-spacing: 0.05em;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-group-title:hover {
color: var(--cb-text);
}
.recipe-list {
list-style: none;
padding: 0;
margin: 0;
}
.recipe-item {
padding: 8px 20px;
cursor: pointer;
font-size: 14px;
color: var(--cb-text-muted);
transition: all 0.2s;
border-left: 3px solid transparent;
}
.recipe-item:hover {
background: rgba(255,255,255,0.05);
color: var(--cb-text);
}
body.light-theme .recipe-item:hover {
background: rgba(0,0,0,0.03);
}
.recipe-item.active {
color: var(--cb-accent);
background: rgba(59, 130, 246, 0.1);
border-left-color: var(--cb-accent);
}
/* Main Content */
.cookbook-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--cb-bg);
}
.content-header {
height: 60px;
padding: 0 30px;
border-bottom: 1px solid var(--cb-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.hamburger {
display: none;
background: transparent;
border: none;
color: var(--cb-text);
font-size: 24px;
cursor: pointer;
}
.breadcrumb {
font-size: 13px;
color: var(--cb-text-muted);
}
.breadcrumb span::after {
content: "/";
margin: 0 8px;
color: var(--cb-border);
}
.breadcrumb span:last-child::after {
display: none;
}
.recipe-content {
flex: 1;
overflow-y: auto;
padding: 40px 60px;
max-width: 1000px;
margin: 0 auto;
width: 100%;
}
.welcome-screen {
text-align: center;
padding-top: 60px;
}
.welcome-screen h1 {
font-size: 32px;
margin-bottom: 16px;
}
.welcome-screen p {
color: var(--cb-text-muted);
font-size: 18px;
}
.stats-overview {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 50px;
}
.stat-card {
background: var(--cb-card-bg);
border: 1px solid var(--cb-border);
padding: 24px;
border-radius: 12px;
min-width: 150px;
text-align: center;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.stat-value {
display: block;
font-size: 28px;
font-weight: 700;
color: var(--cb-accent);
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: var(--cb-text-muted);
text-transform: uppercase;
}
/* Recipe Styles */
.recipe-title {
font-size: 36px;
margin-bottom: 10px;
}
.recipe-desc {
font-size: 18px;
color: var(--cb-text-muted);
margin-bottom: 30px;
line-height: 1.6;
}
.diagram-container {
background: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 40px;
cursor: zoom-in;
text-align: center;
border: 1px solid var(--cb-border);
}
.diagram-container img {
max-width: 100%;
height: auto;
}
.section-title {
font-size: 24px;
margin: 40px 0 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--cb-border);
}
.text-block {
line-height: 1.7;
font-size: 16px;
color: var(--cb-text);
}
.api-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.api-table th {
text-align: left;
padding: 12px;
background: var(--cb-card-bg);
border-bottom: 2px solid var(--cb-border);
font-size: 13px;
color: var(--cb-text-muted);
}
.api-table td {
padding: 12px;
border-bottom: 1px solid var(--cb-border);
font-size: 14px;
}
.method-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
margin-right: 8px;
}
.method-get { background: #0ea5e9; color: white; }
.method-post { background: #10b981; color: white; }
.method-put { background: #f59e0b; color: white; }
.method-delete { background: #ef4444; color: white; }
.code-container {
margin-bottom: 30px;
}
.code-header {
background: #333;
padding: 8px 16px;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #ccc;
}
.btn-copy {
background: transparent;
border: 1px solid #555;
color: #ccc;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
}
.btn-copy:hover {
background: #444;
}
pre {
margin-top: 0 !important;
border-radius: 0 0 8px 8px !important;
}
/* Floating Elements */
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
width: 45px;
height: 45px;
background: var(--cb-accent);
color: white;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 1000;
transition: transform 0.2s;
}
.back-to-top:hover {
transform: translateY(-5px);
}
/* Zoom Overlay */
.zoom-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.95);
display: none;
justify-content: center;
align-items: center;
z-index: 2000;
cursor: zoom-out;
}
.zoom-overlay img {
max-width: 95%;
max-height: 95%;
object-fit: contain;
}
.close-zoom {
position: absolute;
top: 20px; right: 30px;
font-size: 40px;
color: white;
cursor: pointer;
}
/* Responsive */
@media (max-width: 768px) {
.hamburger { display: block; }
.cookbook-sidebar {
position: fixed;
left: 0; top: 0; bottom: 0;
transform: translateX(-100%);
}
.cookbook-sidebar.open {
transform: translateX(0);
box-shadow: 10px 0 30px rgba(0,0,0,0.5);
}
.recipe-content {
padding: 20px;
}
.stats-overview {
flex-direction: column;
align-items: center;
}
.stat-card { width: 100%; max-width: 200px; }
}
/* Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--cb-bg); }
::-webkit-scrollbar-thumb { background: var(--cb-border); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--cb-text-muted); }
.btn-icon {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
color: var(--cb-text);
}
.btn-icon:hover {
opacity: 1;
}
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Canifa AI Platform Documentation & Cookbook — Hướng dẫn chi tiết về kiến trúc và API.">
<meta name="keywords" content="Canifa, AI, Chatbot, Documentation, Cookbook, API">
<title>Canifa AI Platform Cookbook</title>
<link rel="stylesheet" href="../common/theme.css">
<link rel="stylesheet" href="cookbook.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
</head>
<body class="premium-dark">
<div class="cookbook-container">
<!-- Sidebar -->
<aside class="cookbook-sidebar" id="sidebar">
<div class="sidebar-header">
<div class="search-container">
<input type="text" id="recipeSearch" placeholder="Tìm kiếm recipe...">
<span class="search-icon">🔍</span>
</div>
</div>
<nav class="recipe-nav" id="recipeNav">
<!-- Loaded via JS -->
</nav>
</aside>
<!-- Main Content -->
<main class="cookbook-main">
<header class="content-header">
<button id="menuToggle" class="hamburger"></button>
<div class="breadcrumb" id="breadcrumb">
<span>Cookbook</span>
</div>
<div class="header-actions">
<button id="themeToggle" class="btn-icon" title="Đổi giao diện">🌓</button>
<button id="downloadDocx" class="btn-icon" title="Tải DOCX">📄</button>
</div>
</header>
<article class="recipe-content" id="recipeContent">
<div class="welcome-screen">
<h1>📖 Canifa AI Platform Cookbook</h1>
<p>Hướng dẫn chi tiết A-Z về kiến trúc, API và cách tích hợp các module trong hệ thống.</p>
<div class="stats-overview">
<div class="stat-card">
<span class="stat-value">36+</span>
<span class="stat-label">Modules</span>
</div>
<div class="stat-card">
<span class="stat-value">26</span>
<span class="stat-label">Recipes</span>
</div>
<div class="stat-card">
<span class="stat-value" id="progressPercent">0%</span>
<span class="stat-label">Hoàn thành</span>
</div>
</div>
</div>
</article>
</main>
</div>
<!-- Floating Buttons -->
<button id="backToTop" class="back-to-top" title="Quay lại đầu trang"></button>
<!-- Zoom Overlay -->
<div id="zoomOverlay" class="zoom-overlay">
<span class="close-zoom">&times;</span>
<img id="zoomedImage" src="" alt="Zoomed Diagram">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script src="cookbook.js"></script>
</body>
</html>
/**
* Canifa AI Platform Cookbook — Core Logic (Updated)
*/
document.addEventListener('DOMContentLoaded', async () => {
const recipeNav = document.getElementById('recipeNav');
const recipeContent = document.getElementById('recipeContent');
const recipeSearch = document.getElementById('recipeSearch');
const breadcrumb = document.getElementById('breadcrumb');
const zoomOverlay = document.getElementById('zoomOverlay');
const zoomedImage = document.getElementById('zoomedImage');
const closeZoom = document.querySelector('.close-zoom');
const sidebar = document.getElementById('sidebar');
const menuToggle = document.getElementById('menuToggle');
const themeToggle = document.getElementById('themeToggle');
const backToTop = document.getElementById('backToTop');
const mainContainer = document.querySelector('.recipe-content');
let recipesRegistry = [];
// 1. Load Registry
try {
const response = await fetch('data/registry.json');
recipesRegistry = await response.json();
renderSidebar(recipesRegistry);
updateProgress();
} catch (err) {
console.error('Failed to load registry:', err);
}
// 2. Render Sidebar
function renderSidebar(data) {
recipeNav.innerHTML = '';
data.groups.forEach(group => {
const groupEl = document.createElement('div');
groupEl.className = 'nav-group';
const titleEl = document.createElement('div');
titleEl.className = 'nav-group-title';
titleEl.innerHTML = `<span>${group.name}</span> <span class="toggle-icon">▼</span>`;
const listEl = document.createElement('ul');
listEl.className = 'recipe-list';
group.recipes.forEach(recipe => {
const itemEl = document.createElement('li');
itemEl.className = 'recipe-item';
itemEl.textContent = recipe.title;
itemEl.onclick = () => {
loadRecipe(recipe.id, group.name, recipe.title);
if (window.innerWidth <= 768) sidebar.classList.remove('open');
};
listEl.appendChild(itemEl);
});
groupEl.appendChild(titleEl);
groupEl.appendChild(listEl);
recipeNav.appendChild(groupEl);
titleEl.onclick = () => {
const isHidden = listEl.style.display === 'none';
listEl.style.display = isHidden ? 'block' : 'none';
titleEl.querySelector('.toggle-icon').textContent = isHidden ? '▼' : '▶';
};
});
}
// 3. Load Recipe
async function loadRecipe(id, groupName, recipeTitle) {
document.querySelectorAll('.recipe-item').forEach(el => el.classList.remove('active'));
const activeItem = Array.from(document.querySelectorAll('.recipe-item')).find(el => el.textContent === recipeTitle);
if (activeItem) activeItem.classList.add('active');
breadcrumb.innerHTML = `<span>Cookbook</span> <span>${groupName}</span> <span>${recipeTitle}</span>`;
recipeContent.scrollTop = 0;
try {
recipeContent.innerHTML = '<div style="text-align:center; padding-top:100px;">Đang tải recipe...</div>';
const response = await fetch(`data/${id}.json`);
const data = await response.json();
renderRecipe(data);
markAsRead(id);
} catch (err) {
recipeContent.innerHTML = `<div style="color:#ef4444; padding:20px;">Lỗi: Không tìm thấy data cho recipe <b>${id}</b>.</div>`;
}
}
// 4. Render Recipe
function renderRecipe(data) {
let html = `
<h1 class="recipe-title">${data.title}</h1>
<p class="recipe-desc">${data.description}</p>
`;
if (data.diagram) {
html += `
<div class="diagram-container" onclick="zoomDiagram('${data.diagram}')">
<img src="${data.diagram}" alt="${data.title} Architecture">
<p style="font-size: 11px; color: #94a3b8; margin-top: 12px; font-weight: 600;">(CLICK TO ZOOM DIAGRAM)</p>
</div>
`;
}
data.sections.forEach(section => {
html += `<h2 class="section-title">${section.title}</h2>`;
if (section.type === 'text') {
html += `<div class="text-block">${section.content}</div>`;
} else if (section.type === 'api') {
html += `
<div style="overflow-x: auto;">
<table class="api-table">
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Mô tả</th>
</tr>
</thead>
<tbody>
${section.endpoints.map(ep => `
<tr>
<td><span class="method-badge method-${ep.method.toLowerCase()}">${ep.method}</span></td>
<td><code>${ep.path}</code></td>
<td>${ep.description}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else if (section.type === 'code') {
html += `
<div class="code-container">
<div class="code-header">
<span>${section.language.toUpperCase()}</span>
<button class="btn-copy" onclick="copyCode(this)">Copy</button>
</div>
<pre><code class="language-${section.language}">${section.code}</code></pre>
</div>
`;
}
});
recipeContent.innerHTML = html;
if (window.Prism) Prism.highlightAll();
}
// UI Logic
menuToggle.onclick = () => sidebar.classList.toggle('open');
themeToggle.onclick = () => {
document.body.classList.toggle('light-theme');
localStorage.setItem('cb_theme', document.body.classList.contains('light-theme') ? 'light' : 'dark');
};
if (localStorage.getItem('cb_theme') === 'light') {
document.body.classList.add('light-theme');
}
recipeContent.onscroll = () => {
backToTop.style.display = recipeContent.scrollTop > 300 ? 'block' : 'none';
};
backToTop.onclick = () => {
recipeContent.scrollTo({ top: 0, behavior: 'smooth' });
};
recipeSearch.oninput = (e) => {
const term = e.target.value.toLowerCase();
document.querySelectorAll('.recipe-item').forEach(el => {
const visible = el.textContent.toLowerCase().includes(term);
el.style.display = visible ? 'block' : 'none';
});
document.querySelectorAll('.nav-group').forEach(group => {
const hasVisible = Array.from(group.querySelectorAll('.recipe-item')).some(i => i.style.display !== 'none');
group.style.display = hasVisible ? 'block' : 'none';
});
};
window.zoomDiagram = (src) => {
zoomedImage.src = src;
zoomOverlay.style.display = 'flex';
};
zoomOverlay.onclick = () => zoomOverlay.style.display = 'none';
window.copyCode = (btn) => {
const code = btn.parentElement.nextElementSibling.innerText;
navigator.clipboard.writeText(code);
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 2000);
};
function markAsRead(id) {
let read = JSON.parse(localStorage.getItem('cb_read') || '[]');
if (!read.includes(id)) {
read.push(id);
localStorage.setItem('cb_read', JSON.stringify(read));
updateProgress();
}
}
function updateProgress() {
const read = JSON.parse(localStorage.getItem('cb_read') || '[]');
const total = 26;
const percent = Math.min(100, Math.round((read.length / total) * 100));
const progressEl = document.getElementById('progressPercent');
if (progressEl) progressEl.textContent = `${percent}%`;
}
});
{
"title": "Platform Architecture Overview",
"description": "Cái nhìn tổng quan về kiến trúc 5 lớp của Canifa AI Platform, từ giao diện người dùng đến các module API và hạ tầng dữ liệu.",
"diagram": "diagrams/platform-architecture.svg",
"sections": [
{
"title": "Cấu trúc 5 lớp (5-Layer Stack)",
"type": "text",
"content": "Hệ thống được thiết kế theo mô hình micro-modular monolith, cho phép mở rộng nhanh chóng các tính năng AI mà không làm ảnh hưởng đến lõi hệ thống.<br>1. <b>Client Layer:</b> Trình duyệt web chạy <code>main.html</code>, quản lý layout chính và sidebar.<br>2. <b>Interface Layer:</b> Hệ thống iframe cho phép load các module độc lập, giúp cô lập CSS/JS.<br>3. <b>Gateway Layer:</b> FastAPI Router đóng vai trò điều phối tất cả các request API.<br>4. <b>Module Layer:</b> 36+ module nghiệp vụ (AI Agents, Product, Social, v.v.) xử lý logic cụ thể.<br>5. <b>Data Layer:</b> Kết hợp SQLite (local dev), StarRocks (olap), và các dịch vụ external."
},
{
"title": "Danh sách API Modules chính",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/auth/me", "description": "Lấy thông tin user hiện tại và settings LLM." },
{ "method": "POST", "path": "/api/chat", "description": "Endpoint chính cho chatbot tương tác với n8n." },
{ "method": "GET", "path": "/api/live-monitor", "description": "Theo dõi các event realtime qua WebSocket." }
]
},
{
"title": "Cấu trúc Thư mục",
"type": "code",
"language": "bash",
"code": "backend/\n├── api/ # Chứa toàn bộ logic router & business\n├── database/ # SQLite files & Migrations\n├── common/ # Shared utilities (db, auth, logs)\n├── static/ # Frontend assets (HTML, CSS, JS)\n└── server.py # Entry point của ứng dụng"
}
]
}
{
"title": "Database Layer & SQLite Mock",
"description": "Chi tiết về cách hệ thống xử lý dữ liệu đồng bộ và bất đồng bộ, cùng cơ chế SQLite Mock để giả lập Postgres trong môi trường phát triển.",
"diagram": "diagrams/database-layer.svg",
"sections": [
{
"title": "Cơ chế SQL Translation",
"type": "text",
"content": "Để hỗ trợ chạy local mà không cần Postgres/StarRocks, module <code>sqlite_mock.py</code> thực hiện đánh chặn các câu lệnh SQL và dịch chúng sang dialect của SQLite thông qua hàm <code>translate_query()</code>. Các quy tắc chính bao gồm:<br>- Chuyển <code>%s</code> thành <code>?</code><br>- Thay thế tên schema (ví dụ: <code>public.users</code> thành <code>pg__public__users</code>)<br>- Giả lập các hàm đặc thù như <code>ANY_VALUE</code>, <code>MAX_BY</code>, <code>ILIKE</code>."
},
{
"title": "Async vs Sync Paths",
"type": "text",
"content": "1. <b>Async Path:</b> Sử dụng <code>aiosqlite</code> cho các API route hiện đại. Luồng này truy cập trực tiếp vào file <code>canifa_ai_dump.sqlite</code>.<br>2. <b>Sync Path:</b> Sử dụng <code>MockCursor</code> để tương thích với các legacy helper classes (như UltraDescDB) mà không cần refactor hàng chục method sang async."
},
{
"title": "Cấu hình Kết nối",
"type": "code",
"language": "python",
"code": "from common.pool_wrapper import get_pooled_connection_compat\n\n# Cách lấy connection tương thích cả Prod (PG) và Dev (SQLite Mock)\nwith get_pooled_connection_compat() as conn:\n with conn.cursor() as cur:\n cur.execute(\"SELECT * FROM public.users WHERE id = %s\", (user_id,))\n user = cur.fetchone()"
}
]
}
{
"title": "Auth & Routing Flow",
"description": "Tìm hiểu cách hệ thống xác thực người dùng bằng JWT và cơ chế điều hướng trang (routing) thông qua Single Sidebar.",
"diagram": "diagrams/auth-routing.svg",
"sections": [
{
"title": "Cơ chế Xác thực (JWT)",
"type": "text",
"content": "Hệ thống sử dụng JWT (JSON Web Token) để quản lý phiên làm việc. Khi login thành công, token được lưu vào <code>localStorage</code> và gửi kèm trong header <code>Authorization: Bearer <token></code> cho mọi request API.<br>Module <code>auth.js</code> phía client chịu trách nhiệm kiểm tra token (guard) mỗi khi trang web được tải lại."
},
{
"title": "Single Sidebar Navigation",
"type": "text",
"content": "Toàn bộ giao diện Platform nằm trong <code>main.html</code>. Khi người dùng click vào một mục trên sidebar, hàm <code>navigateTo(page)</code> sẽ:<br>1. Cập nhật thuộc tính <code>src</code> của Iframe.<br>2. Thêm tham số <code>t=<timestamp></code> để tránh cache trình duyệt.<br>3. Sử dụng <code>history.pushState</code> để cập nhật URL mà không làm tải lại trang."
},
{
"title": "API Authentication Endpoints",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/auth/login", "description": "Xác thực user và trả về access_token." },
{ "method": "GET", "path": "/api/auth/me", "description": "Lấy profile và settings của user hiện tại." },
{ "method": "PUT", "path": "/api/auth/me/settings", "description": "Cập nhật Codex Token hoặc OpenAI Key cá nhân." }
]
}
]
}
{
"title": "Chatbot Core & n8n Pipeline",
"description": "Khám phá luồng xử lý tin nhắn của Fashion Q&A Agent, từ lúc nhận query đến khi gọi các tool n8n và trả về kết quả cho người dùng.",
"diagram": "diagrams/chatbot-pipeline.svg",
"sections": [
{
"title": "Luồng xử lý (Execution Flow)",
"type": "text",
"content": "1. <b>Chat Controller:</b> Tiếp nhận request, kiểm tra Redis cache. Nếu miss, khởi tạo LangGraph engine.<br>2. <b>LangGraph Agent:</b> Chạy vòng lặp suy nghĩ (Reasoning Loop). Agent quyết định có cần gọi công cụ (tools) hay không.<br>3. <b>Tool Execution:</b> Nếu cần dữ liệu, Agent gọi các API chuyên dụng (n8n API) để lấy thông tin sản phẩm, tồn kho, cửa hàng.<br>4. <b>Response Synthesis:</b> Kết hợp dữ liệu thô từ tools và kiến thức thời trang để tạo câu trả lời tự nhiên."
},
{
"title": "Danh sách API n8n Tools",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/agent/n8n/products", "description": "Lấy mẫu sản phẩm hoặc tìm kiếm theo SKU/tên." },
{ "method": "GET", "path": "/api/agent/n8n/stock", "description": "Kiểm tra tồn kho realtime của Canifa." },
{ "method": "GET", "path": "/api/agent/n8n/stores", "description": "Tìm kiếm danh sách cửa hàng theo khu vực." },
{ "method": "GET", "path": "/api/agent/n8n/knowledge", "description": "Tra cứu chính sách đổi trả, bảng size (RAG)." }
]
},
{
"title": "Cấu trúc Request",
"type": "code",
"language": "json",
"code": "{\n \"user_query\": \"Tìm cho mình áo phông nam màu xanh size L còn hàng ở Hà Nội\",\n \"images\": [],\n \"history_limit\": 15\n}"
}
]
}
{
"title": "Prompt & Tool Management",
"description": "Hướng dẫn cách quản lý System Prompt và các Tool Prompt linh hoạt mà không cần khởi động lại server.",
"diagram": "diagrams/prompt-management.svg",
"sections": [
{
"title": "Cơ chế Dynamic Prompt",
"type": "text",
"content": "Hệ thống lưu trữ prompt trong các file <code>.txt</code> tại thư mục <code>backend/agent/</code>. Mỗi khi Admin cập nhật prompt qua UI, server sẽ ghi đè vào file tương ứng và gọi hàm <code>reset_chain_cache()</code> để xóa cache logic của LangChain. Lần truy vấn kế tiếp, Agent sẽ tự động nhận diện sự thay đổi nội dung (via MD5 hash) và nạp lại prompt mới."
},
{
"title": "Phân loại Prompt",
"type": "text",
"content": "1. <b>System Prompt:</b> Định nghĩa nhân cách, kiến thức cốt lõi và phong cách trả lời của Agent.<br>2. <b>Tool Prompts:</b> Định nghĩa chi tiết cách Agent sử dụng từng công cụ (ví dụ: cách tìm sản phẩm, cách tra cứu tồn kho).<br>3. <b>User Insight Template:</b> Định nghĩa các thông tin cần thu thập về người dùng (giới tính, sở thích, size)."
},
{
"title": "API Quản lý Prompt",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/agent/system-prompt", "description": "Lấy nội dung System Prompt hiện tại." },
{ "method": "POST", "path": "/api/agent/system-prompt", "description": "Cập nhật System Prompt và làm mới cache." },
{ "method": "GET", "path": "/api/agent/tool-prompts", "description": "Liệt kê danh sách các tool prompt có sẵn." },
{ "method": "POST", "path": "/api/agent/tool-prompts/{filename}", "description": "Cập nhật nội dung một tool prompt cụ thể." }
]
}
]
}
{
"title": "Feedback & Diagram Agents",
"description": "Tìm hiểu cách hệ thống phân loại phản hồi từ người dùng và Agent chuyên biệt để tạo sơ đồ kỹ thuật.",
"diagram": "diagrams/feedback-pipeline.svg",
"sections": [
{
"title": "Hệ thống Phân loại Feedback",
"type": "text",
"content": "Mỗi khi người dùng Like/Dislike hoặc để lại bình luận, request được gửi tới <code>/api/feedback</code>. Server phản hồi ngay lập tức và đẩy xử lý vào Background Task:<br>1. Sử dụng <b>GPT-4o-mini</b> để phân tích cảm xúc và phân loại lỗi (sai sản phẩm, lỗi logic, v.v.).<br>2. Đẩy điểm số (score) lên <b>Langfuse</b> gắn với <code>trace_id</code> để theo dõi chất lượng LLM.<br>3. Lưu vào cơ sở dữ liệu local để Admin kiểm tra định kỳ."
},
{
"title": "Diagram Agent",
"type": "text",
"content": "Module Diagram Agent cho phép người dùng mô tả kiến trúc và nhận lại mã <b>Mermaid</b> để hiển thị sơ đồ. Agent sử dụng mô hình 2-Agent (Planner và Responder) để đảm bảo độ chính xác của cú pháp sơ đồ."
},
{
"title": "API Endpoints",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/feedback", "description": "Gửi phản hồi Like/Dislike/Comment." },
{ "method": "GET", "path": "/api/feedback/stats", "description": "Xem thống kê phản hồi theo category." },
{ "method": "POST", "path": "/api/diagram/chat", "description": "Chat với Agent để sinh sơ đồ Mermaid." }
]
}
]
}
{
"title": "Ultra Description Pipeline",
"description": "Tìm hiểu quy trình 2 giai đoạn (Vision + Enrichment) để tạo ra các bản mô tả sản phẩm siêu chi tiết từ hình ảnh và dữ liệu tồn kho.",
"diagram": "diagrams/platform-architecture.svg",
"sections": [
{
"title": "Quy trình AI Generation",
"type": "text",
"content": "1. <b>Giai đoạn Vision:</b> Sử dụng model <b>Llama-4-Scout</b> để phân tích ảnh sản phẩm, trích xuất 28 trường dữ liệu thô (màu sắc, thiết kế, túi, cổ áo, v.v.).<br>2. <b>Giai đoạn Enrichment:</b> Sử dụng <b>GPT-OSS 120B</b> để viết lại nội dung theo phong cách chuyên gia Stylist, bổ sung hướng dẫn bảo quản và mix-match.<br>3. <b>Persistance:</b> Lưu trữ kết quả vào bảng <code>ultra_descriptions</code> (Postgres) và đánh dấu trạng thái <i>Pending</i> để chờ Admin duyệt."
},
{
"title": "Cấu trúc 28 trường dữ liệu",
"type": "code",
"language": "json",
"code": "{\n \"ten_san_pham\": \"Áo Phông Nam Có Túi\",\n \"chat_lieu\": \"100% Cotton\",\n \"phoi_do\": \"Quần Jean + Sneaker\",\n \"dip_mac\": \"Đi chơi · Đi làm\",\n \"faq_1_q\": \"Áo có bị phai màu không?\",\n \"faq_1_a\": \"Chất liệu Cotton USA cao cấp giúp giữ màu bền bỉ...\"\n}"
},
{
"title": "API Quản lý Mô tả",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/product-desc/overview", "description": "Xem thống kê độ phủ của mô tả AI." },
{ "method": "POST", "path": "/api/product-desc/generate", "description": "Sinh mô tả mới cho 1 sản phẩm cụ thể." },
{ "method": "POST", "path": "/api/product-desc/batch-generate", "description": "Đưa danh sách sản phẩm vào queue xử lý hàng loạt." }
]
}
]
}
{
"title": "Fashion Matching Engine",
"description": "Khám phá thuật toán Stylist AI đằng sau các gợi ý phối đồ (Outfits), dựa trên hệ thống quy tắc thời trang và ma trận phối màu.",
"diagram": "diagrams/fashion-matches.svg",
"sections": [
{
"title": "Cơ chế hoạt động",
"type": "text",
"content": "Engine phối đồ hoạt động theo 5 bước chính:<br>1. <b>Phân tích Anchor:</b> Xác định loại sản phẩm, giới tính và màu sắc của item gốc.<br>2. <b>Tra cứu Quy tắc:</b> Tìm các công thức phối hợp trong bảng <code>chatbot_fashion_rules</code> (ví dụ: Áo Polo nam hợp với Quần Khaki hoặc Quần Jean).<br>3. <b>Ma trận Màu sắc:</b> Tính toán synergy score giữa màu anchor và màu các item ứng viên dựa trên ma trận 16x16 màu.<br>4. <b>Lọc Điều kiện:</b> Loại bỏ các item hết hàng hoặc lệch mùa.<br>5. <b>Xếp hạng:</b> Trả về Top 3 item tốt nhất cho mỗi vị trí (Top, Bottom, Outerwear, Accessory)."
},
{
"title": "Ma trận Phối màu (Color Synergy)",
"type": "text",
"content": "Màu sắc được chia thành 3 nhóm: <b>Neutral</b> (Trắng, Đen, Be, Xám), <b>Light</b> (Hồng, Vàng, Xanh lam), và <b>Dark</b> (Đỏ, Cam, Xanh navy).<br>- Neutral + Any: Điểm cao (An toàn).<br>- Light + Light hoặc Dark + Dark: Điểm trung bình (Cần cân nhắc).<br>- Tương phản mạnh: Điểm cao (Cá tính)."
},
{
"title": "API Phối đồ",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/fashion-matches/{code}", "description": "Lấy danh sách các item phối cùng đã tính toán sẵn." },
{ "method": "POST", "path": "/api/fashion-matches/outfit-suggest", "description": "Gợi ý outfit đầy đủ theo dịp (Occasion) cụ thể." },
{ "method": "GET", "path": "/api/fashion-matches/rules/config", "description": "Lấy cấu hình các quy tắc thời trang." }
]
}
]
}
{
"title": "Product Perf & Stock Cache",
"description": "Hướng dẫn cách hệ thống truy xuất dữ liệu hiệu năng sản phẩm (KPIs) từ StarRocks và kiểm tra tồn kho realtime qua cơ chế parallel async fetch.",
"diagram": "diagrams/stock-cache.svg",
"sections": [
{
"title": "Product Performance (StarRocks)",
"type": "text",
"content": "Dashboard hiệu năng sử dụng <b>StarRocks</b> làm OLAP engine để tính toán các chỉ số Aggregate (Tổng bán, Tồn kho, Giá trung bình) trên hàng triệu bản ghi Magento trong thời gian thực.<br>Dữ liệu được nhóm theo <code>internal_ref_code</code> để hiển thị cái nhìn tổng quan theo mẫu mã thay vì từng màu/size riêng lẻ."
},
{
"title": "Cơ chế Check Tồn kho Realtime",
"type": "text",
"content": "Vì API tồn kho của Canifa chỉ nhận tối đa 200 SKU/lần, hệ thống triển khai cơ chế <b>Parallel Fetching</b>:<br>1. <b>Expand:</b> Chuyển đổi mã gốc sang toàn bộ bộ SKU (Màu-Size) hợp lệ.<br>2. <b>Chunking:</b> Chia danh sách thành các nhóm nhỏ (50 item/nhóm).<br>3. <b>Async Call:</b> Sử dụng <code>httpx.AsyncClient</code> để gọi đồng thời nhiều request, giảm latency tổng thể xuống dưới 2 giây."
},
{
"title": "API Hiệu năng & Tồn kho",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/products/overview", "description": "Lấy các chỉ số KPI tổng quát của toàn bộ kho hàng." },
{ "method": "GET", "path": "/api/products/list", "description": "Danh sách sản phẩm kèm hiệu số bán chạy và tìm kiếm thông minh." },
{ "method": "POST", "path": "/api/stock/check", "description": "Kiểm tra tồn kho realtime cho danh sách sản phẩm." }
]
}
]
}
{
"title": "Lead Search Agent (LangGraph)",
"description": "Khám phá kiến trúc 2-Agent chuyên biệt cho việc tư vấn bán hàng và chốt lead, sử dụng LangGraph để điều phối luồng suy nghĩ.",
"diagram": "diagrams/lead-search-agent.svg",
"sections": [
{
"title": "Kiến trúc 2-Agent (Dual-Agent)",
"type": "text",
"content": "Thay vì sử dụng một model duy nhất, Lead Search Agent chia làm 2 giai đoạn:<br>1. <b>Classifier Node (NHẸ):</b> Sử dụng Llama-4-Scout để phân loại ý định người dùng (Intent). Nếu cần dữ liệu, nó sẽ quyết định gọi <code>lead_search_tool</code>. Nếu là câu hỏi xã giao, nó sẽ Early Exit.<br>2. <b>Stylist Node (NẶNG):</b> Sử dụng GPT-OSS 120B để tổng hợp dữ liệu từ tool result, lịch sử chat và User Insight để đưa ra câu trả lời tư vấn chuyên sâu, đồng thời cập nhật lại 12 trường trong <code>InsightJSON</code>."
},
{
"title": "Cơ chế Insight JSON",
"type": "text",
"content": "Hệ thống duy trì một 'bộ nhớ' cấu trúc cho mỗi khách hàng bao gồm:<br>- <b>TARGET:</b> Đối tượng mua hàng (vợ, chồng, con).<br>- <b>GOAL:</b> Mục đích (mua đi làm, đi chơi).<br>- <b>STAGE:</b> Giai đoạn trong phễu bán hàng (BROWSE, COMPARE, COMMIT).<br>- <b>LAST_ACTION:</b> Hành động cuối cùng của Bot để duy trì context."
},
{
"title": "API Lead Flow",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/agent/chat-lead-flow", "description": "Endpoint chính cho luồng tư vấn Lead Stage." },
{ "method": "POST", "path": "/api/agent/lead-stage", "description": "Chỉ chạy Classifier để debug logic định tuyến." },
{ "method": "GET", "path": "/api/lead/history", "description": "Lấy lịch sử chat Lead Flow từ Postgres (Persistent)." }
]
}
]
}
{
"title": "AI Image Search",
"description": "Hướng dẫn cách hệ thống tìm kiếm sản phẩm thông qua hình ảnh bằng cách kết hợp Vision LLM và Vector Similarity Search.",
"diagram": "diagrams/image-search.svg",
"sections": [
{
"title": "Quy trình Tìm kiếm Đa phương thức",
"type": "text",
"content": "1. <b>Vision Analysis:</b> Khi người dùng tải ảnh lên, Vision LLM (Llama-4 Scout hoặc GPT-4o) sẽ phân tích các thuộc tính thị giác như: loại sản phẩm, màu sắc, họa tiết, chất liệu vải.<br>2. <b>Embedding Generation:</b> Mô tả văn bản từ bước 1 được chuyển thành vector đặc trưng (Embedding).<br>3. <b>StarRocks Vector Search:</b> Thực hiện tìm kiếm láng giềng gần nhất (ANN) trên bảng <code>magento_product_dimension_with_text_embedding</code> để tìm ra các sản phẩm có độ tương đồng cao nhất về mặt ngữ nghĩa.<br>4. <b>Reranking:</b> Stylist Agent sẽ lọc lại danh sách dựa trên giới tính và độ tuổi của khách hàng trước khi trả về kết quả."
},
{
"title": "Kỹ thuật tối ưu",
"type": "text",
"content": "Hệ thống sử dụng hàm <code>approx_cosine_similarity</code> của StarRocks để đảm bảo tốc độ phản hồi dưới 500ms cho hàng chục ngàn sản phẩm."
},
{
"title": "API Image Search",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/image-search/chat", "description": "Gửi ảnh (base64) và text query để tìm sản phẩm tương đương." }
]
}
]
}
{
"title": "SKU & Store Search",
"description": "Hướng dẫn chi tiết về cơ chế parse mã sản phẩm (SKU) bằng Regex và thuật toán tìm kiếm cửa hàng theo vị trí địa lý.",
"diagram": "diagrams/sku-store-search.svg",
"sections": [
{
"title": "SKU Parsing Logic",
"type": "text",
"content": "Hệ thống sử dụng biểu thức chính quy (Regex) để nhận diện mã sản phẩm Canifa ngay trong câu chat. Nếu tìm thấy mã khớp định dạng <code>^[0-9][A-Z]{2}[0-9]{2}[A-Z][0-9]{3}</code>, Agent sẽ ưu tiên tra cứu trực tiếp trong database thay vì sử dụng Vector Search, giúp đảm bảo độ chính xác 100% khi khách hàng hỏi về một mã cụ thể."
},
{
"title": "Thuật toán Tìm kiếm Cửa hàng",
"type": "text",
"content": "Engine tìm kiếm cửa hàng thực hiện qua 2 bước:<br>1. <b>Chuẩn hóa:</b> Loại bỏ các tiền tố như 'Quận', 'Huyện', 'TP' để lấy keyword lõi (ví dụ: 'Cầu Giấy').<br>2. <b>Multi-token LIKE:</b> Chia nhỏ địa điểm thành các token và thực hiện câu lệnh SQL <code>LIKE</code> trên nhiều cột (Tên CH, Địa chỉ, Thành phố).<br>3. <b>Schedule Integration:</b> Trả về trạng thái đóng/mở cửa dựa trên thời gian thực hiện tại."
},
{
"title": "API Tra cứu",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/agent/n8n/products?q={sku}", "description": "Tra cứu nhanh thông tin sản phẩm theo mã SKU." },
{ "method": "GET", "path": "/api/agent/n8n/stores?location={city}", "description": "Tìm danh sách cửa hàng tại một khu vực nhất định." }
]
}
]
}
{
"title": "Text-to-SQL & Data Analyst",
"description": "Tìm hiểu cách hệ thống chuyển đổi ngôn ngữ tự nhiên thành các câu lệnh SQL phức tạp để phân tích dữ liệu bán hàng trên StarRocks.",
"diagram": "diagrams/text-to-sql.svg",
"sections": [
{
"title": "Cơ chế Text-to-SQL",
"type": "text",
"content": "Module sử dụng Agent có khả năng tự nhận diện Schema (Schema Awareness). Thay vì nhét toàn bộ DDL vào prompt, Agent thực hiện qua các bước:<br>1. <b>Table Selection:</b> Chọn các bảng liên quan đến câu hỏi (Sản phẩm, Đơn hàng, Tồn kho).<br>2. <b>SQL Generation:</b> Sinh câu lệnh SQL dialect StarRocks (tương thích MySQL).<br>3. <b>Execution:</b> Chạy query trên database StarRocks và nhận kết quả JSON.<br>4. <b>Self-Correction:</b> Nếu SQL lỗi, Agent sẽ đọc thông báo lỗi và thử sinh lại tối đa 3 lần."
},
{
"title": "AI Data Analyst",
"type": "text",
"content": "Sau khi có dữ liệu thô, Analyst Node sẽ:<br>- <b>Summarization:</b> Tóm tắt các con số quan trọng (Tổng doanh thu, % tăng trưởng).<br>- <b>Insight Extraction:</b> Tìm ra các điểm bất thường hoặc xu hướng (ví dụ: 'Áo phông nam đang bán chậm ở miền Nam').<br>- <b>Visualization Suggestion:</b> Đề xuất loại biểu đồ phù hợp (Cột, Tròn, Đường) để frontend hiển thị."
},
{
"title": "API SQL & Insights",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/sql/chat", "description": "Chat với dữ liệu để lấy insight và biểu đồ." },
{ "method": "POST", "path": "/api/sql/generate", "description": "Chỉ sinh code SQL từ ngôn ngữ tự nhiên (cho Admin)." },
{ "method": "GET", "path": "/api/sql/user-insight/{id}", "description": "Lấy hồ sơ insight chi tiết của 1 khách hàng cụ thể." }
]
}
]
}
{
"title": "Social Inbox & CRM",
"description": "Tìm hiểu cách hệ thống quản lý tin nhắn và bình luận từ đa nền tảng mạng xã hội, phân tích cảm xúc và đưa vào vòng lặp học tập (learning loop).",
"diagram": "diagrams/social-content-flow.svg",
"sections": [
{
"title": "Xử lý Webhook Đa nền tảng",
"type": "text",
"content": "Hệ thống tiếp nhận dữ liệu thời gian thực thông qua các Webhook endpoint chuyên biệt cho Facebook/Instagram và TikTok. Quy trình xử lý bao gồm:<br>1. <b>Signature Verification:</b> Kiểm tra tính hợp lệ của request (X-Hub-Signature-256).<br>2. <b>Normalization:</b> Chuyển đổi payload đặc thù của từng nền tảng về định dạng <code>SocialMessage</code> chung.<br>3. <b>Sentiment Analysis:</b> Sử dụng module <code>analyze_sentiment_detail</code> để phân loại cảm xúc (Tích cực, Tiêu cực, Trung lập)."
},
{
"title": "Vòng lặp Học tập (Learning Loop)",
"type": "text",
"content": "Các bình luận tiêu cực (Negative) sẽ được tự động đẩy vào <code>feedback_tracker</code>. Tại đây, AI sẽ tóm tắt nội dung khiếu nại và gán độ nghiêm trọng (severity). Dữ liệu này giúp Admin nhận diện các điểm yếu trong sản phẩm hoặc dịch vụ để cải thiện prompt cho Chatbot trong tương lai."
},
{
"title": "API Social Inbox",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/social/webhook/facebook", "description": "Nhận sự kiện từ Facebook Graph API." },
{ "method": "GET", "path": "/api/social/messages", "description": "Lấy danh sách tin nhắn/bình luận đã lưu kèm phân tích sentiment." },
{ "method": "GET", "path": "/api/social/messages/stats", "description": "Xem thống kê tỷ lệ phản hồi tiêu cực theo platform." }
]
}
]
}
{
"title": "AI Content Composer",
"description": "Hướng dẫn sử dụng công cụ soạn thảo nội dung AI để tạo bài viết marketing chuẩn Canifa cho các mạng xã hội.",
"diagram": "diagrams/social-content-flow.svg",
"sections": [
{
"title": "Cơ chế Soạn thảo (Composition)",
"type": "text",
"content": "Content Composer sử dụng các <b>Content Templates</b> đã được định nghĩa sẵn để đảm bảo tính nhất quán về brand voice. Người dùng chỉ cần nhập chủ đề (Topic) hoặc mã sản phẩm, AI sẽ tự động:<br>- Tra cứu đặc điểm nổi bật của sản phẩm (từ Ultra Description).<br>- Lồng ghép các chương trình khuyến mãi hiện có.<br>- Thêm các hashtag phổ biến và icon trang trí phù hợp với từng platform (FB vs TikTok)."
},
{
"title": "Tích hợp Media",
"type": "text",
"content": "Hệ thống kết nối trực tiếp với <b>Media Library</b>, cho phép kéo thả hình ảnh sản phẩm Magento hoặc ảnh marketing từ cloud vào bài viết."
},
{
"title": "API Content & Templates",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/content/templates", "description": "Liệt kê danh sách các mẫu bài viết (Sale, New Arrival, v.v.)." },
{ "method": "POST", "path": "/api/content/generate", "description": "Yêu cầu AI viết nháp bài đăng dựa trên template và sản phẩm." },
{ "method": "GET", "path": "/api/content/queue", "description": "Xem danh sách các bài viết đang chờ đăng." }
]
}
]
}
{
"title": "Approval Workflow",
"description": "Quy trình kiểm duyệt nội dung do AI sinh ra trước khi xuất bản lên các kênh chính thức của thương hiệu.",
"diagram": "diagrams/social-content-flow.svg",
"sections": [
{
"title": "Trạng thái Nội dung",
"type": "text",
"content": "Mỗi bản nháp nội dung (Content Draft) sẽ trải qua các trạng thái:<br>1. <b>Draft:</b> Vừa được AI sinh ra.<br>2. <b>Pending:</b> Đang chờ Admin duyệt.<br>3. <b>Approved:</b> Đã duyệt, chuyển vào hàng đợi đăng bài.<br>4. <b>Rejected:</b> Cần sửa lại (Admin có thể để lại comment feedback cho AI viết lại)."
},
{
"title": "API Kiểm duyệt",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/approval/list", "description": "Lấy danh sách các yêu cầu kiểm duyệt đang chờ." },
{ "method": "POST", "path": "/api/approval/{id}/approve", "description": "Chấp thuận nội dung và đặt lịch đăng." },
{ "method": "POST", "path": "/api/approval/{id}/reject", "description": "Từ chối nội dung kèm lý do." }
]
}
]
}
{
"title": "Media Asset Library",
"description": "Kho lưu trữ tập trung các tài nguyên hình ảnh, video phục vụ cho việc tạo nội dung marketing.",
"diagram": "diagrams/social-content-flow.svg",
"sections": [
{
"title": "Phân loại Tài nguyên",
"type": "text",
"content": "Media Library chứa 2 nhóm tài nguyên chính:<br>1. <b>Magento Sync:</b> Toàn bộ ảnh sản phẩm được đồng bộ tự động từ website canifa.com.<br>2. <b>Cloud Marketing:</b> Các bộ sưu tập ảnh chụp lookbook, video quảng cáo do team Creative tải lên."
},
{
"title": "Tối ưu hình ảnh",
"type": "text",
"content": "Hệ thống tự động sinh <b>Thumbnail</b> (ảnh thu nhỏ) để tăng tốc độ tải trang Dashboard và hỗ trợ preview nhanh trong Content Composer."
},
{
"title": "API Media",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/media/search", "description": "Tìm kiếm ảnh theo tên sản phẩm hoặc tag." },
{ "method": "POST", "path": "/api/media/upload", "description": "Tải file media mới lên hệ thống." },
{ "method": "DELETE", "path": "/api/media/{id}", "description": "Xóa tài nguyên khỏi thư viện." }
]
}
]
}
{
"title": "Realtime Monitor (WebSocket)",
"description": "Hướng dẫn vận hành hệ thống giám sát hoạt động AI thời gian thực thông qua kết nối SSE (Server-Sent Events) và Langfuse REST API.",
"diagram": "diagrams/realtime-monitor.svg",
"sections": [
{
"title": "Cơ chế Live Stream",
"type": "text",
"content": "Thay vì sử dụng Polling liên tục, dashboard Live Monitor sử dụng <b>SSE (Server-Sent Events)</b> để nhận dữ liệu đẩy từ server. Server duy trì một event loop mỗi 4 giây:<br>1. Gọi <b>Langfuse API</b> để lấy snapshot các trace mới nhất.<br>2. Tính toán các chỉ số Aggregate (Latency trung bình, Error Rate, Token Cost).<br>3. Kiểm tra <b>Signature</b> của dữ liệu. Nếu có thay đổi so với lần trước, server sẽ đẩy payload mới xuống client."
},
{
"title": "Các chỉ số Giám sát",
"type": "text",
"content": "Dashboard cung cấp 4 biểu đồ quan trọng:<br>- <b>Traffic:</b> Số lượng request mỗi phút.<br>- <b>Latency:</b> Tốc độ phản hồi của LLM (cảnh báo nếu > 12s).<br>- <b>Cost:</b> Chi phí vận hành ước tính theo thời gian thực.<br>- <b>Error Rate:</b> Tỷ lệ lỗi hệ thống hoặc lỗi từ model provider."
},
{
"title": "API Giám sát",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/live-monitor/bootstrap", "description": "Lấy dữ liệu khởi tạo cho dashboard (20 phút gần nhất)." },
{ "method": "GET", "path": "/api/live-monitor/stream", "description": "Mở kết nối SSE để nhận update liên tục." }
]
}
]
}
{
"title": "Regression Testing Suite",
"description": "Hướng dẫn sử dụng bộ kiểm thử hồi quy để đảm bảo chất lượng câu trả lời của LLM không bị suy giảm sau mỗi lần cập nhật Prompt hoặc Model.",
"diagram": "diagrams/testing-suite.svg",
"sections": [
{
"title": "Cơ chế Kiểm thử Hồi quy",
"type": "text",
"content": "Regression Test hoạt động bằng cách gửi một danh sách các câu hỏi tiêu chuẩn (Golden Dataset) tới chatbot và thu thập câu trả lời. Admin sẽ review các kết quả này để đánh giá độ chính xác về thông tin sản phẩm, brand voice và khả năng xử lý tình huống.<br>Hệ thống hỗ trợ so sánh song song giữa môi trường <b>Prod</b> và <b>Dev</b> để nhận diện sự khác biệt."
},
{
"title": "API Regression Test",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/regression-test/run", "description": "Chạy toàn bộ test cases đối với một endpoint cụ thể." },
{ "method": "GET", "path": "/api/regression-test/history", "description": "Xem lịch sử các lần chạy test và tỷ lệ pass/fail." }
]
}
]
}
{
"title": "Stress & Load Testing",
"description": "Hướng dẫn thực hiện kiểm thử tải để xác định giới hạn chịu tải của hệ thống và latency ở mức concurrency cao.",
"diagram": "diagrams/testing-suite.svg",
"sections": [
{
"title": "Stress Test với Locust",
"type": "text",
"content": "Hệ thống tích hợp sẵn <b>Locust</b> để giả lập hàng trăm người dùng truy cập đồng thời. Stress test giúp đo lường:<br>- <b>RPS (Requests Per Second):</b> Số lượng request tối đa hệ thống xử lý được.<br>- <b>P99 Latency:</b> Thời gian phản hồi mà 99% người dùng gặp phải.<br>- <b>Model Rate Limit:</b> Giới hạn từ phía nhà cung cấp LLM (OpenAI/Anthropic)."
},
{
"title": "API Stress Test",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/stress-test/start", "description": "Khởi động một worker Locust để bắt đầu spam request." },
{ "method": "GET", "path": "/api/stress-test/stats", "description": "Lấy báo cáo realtime về hiệu năng server dưới tải." }
]
}
]
}
{
"title": "User Persona Simulation",
"description": "Sử dụng AI để đóng vai các 'Persona' khách hàng khác nhau nhằm thực hiện kiểm thử thăm dò (Exploratory Testing).",
"diagram": "diagrams/testing-suite.svg",
"sections": [
{
"title": "Cơ chế Simulator",
"type": "text",
"content": "User Simulator tạo ra các chatbot ảo có tính cách và nhu cầu cụ thể (ví dụ: 'Bà mẹ bỉm sữa khó tính', 'Nam thanh niên săn sale', 'Khách hàng hỏi size cho con'). Các chatbot này sẽ tự động trò chuyện với hệ thống để:<br>- Tìm ra các trường hợp xử lý lỗi hoặc thiếu logic.<br>- Kiểm tra khả năng chốt sale của Agent.<br>- Verify tính nhất quán của User Insight JSON qua nhiều turn chat."
},
{
"title": "API Simulator",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/user-simulator/spawn", "description": "Tạo ra một phiên chat giả lập với một persona nhất định." },
{ "method": "GET", "path": "/api/user-simulator/personas", "description": "Liệt kê các thư viện tính cách khách hàng có sẵn." }
]
}
]
}
{
"title": "Reaction & Emoji Simulation",
"description": "Giả lập hàng loạt phản hồi người dùng (Like/Dislike) để kiểm tra độ chịu tải và độ chính xác của bộ phân loại Feedback.",
"diagram": "diagrams/testing-suite.svg",
"sections": [
{
"title": "Stress Testing Feedback Loop",
"type": "text",
"content": "Vì mỗi feedback sẽ kích hoạt một Background Task gọi LLM để phân loại, việc giả lập reaction giúp verify:<br>1. <b>Queue Depth:</b> Khả năng xếp hàng các task xử lý sentiment.<br>2. <b>Langfuse Rate Limit:</b> Đảm bảo không làm nghẽn API đẩy score lên Langfuse.<br>3. <b>Dashboard Realtime:</b> Kiểm tra khả năng cập nhật biểu đồ thống kê khi có hàng ngàn feedback đổ về."
},
{
"title": "API Reaction Sim",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/reaction-simulator/spam", "description": "Gửi ngẫu nhiên N feedback (Like/Dislike) vào hệ thống." }
]
}
]
}
{
"title": "FAQ Manager & BM25 Search",
"description": "Tìm hiểu cơ chế quản lý câu hỏi thường gặp và thuật toán tìm kiếm lai (Hybrid Search) kết hợp BM25 và Fuzzy Matching.",
"diagram": "diagrams/faq-bm25-search.svg",
"sections": [
{
"title": "Thuật toán Hybrid Search",
"type": "text",
"content": "Để đảm bảo tìm đúng câu hỏi ngay cả khi người dùng viết tắt hoặc gõ sai, hệ thống sử dụng thuật toán <b>Simulate Match</b>:<br>1. <b>BM25 (Best Matching 25):</b> Thuật toán xếp hạng dựa trên tần suất từ khóa, ưu tiên các từ hiếm và mang ý nghĩa quan trọng.<br>2. <b>Fuzzy Match:</b> Sử dụng khoảng cách Levenshtein để xử lý các lỗi chính tả nhẹ (ví dụ: 'quan jean' vs 'quần jean').<br>3. <b>Score Fusion:</b> Kết hợp điểm số từ hai thuật toán theo trọng số 70/30 để đưa ra kết quả cuối cùng."
},
{
"title": "AI Variant Generation",
"type": "text",
"content": "Module tích hợp GPT-4.1-mini để tự động sinh ra 20-50 biến thể cho mỗi câu hỏi gốc. Việc này giúp mở rộng bề mặt tìm kiếm, giúp hệ thống nhận diện được nhiều cách đặt câu hỏi khác nhau của khách hàng mà không cần Admin phải nhập liệu thủ công."
},
{
"title": "API FAQ",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/faqs", "description": "Lấy danh sách câu hỏi và câu trả lời." },
{ "method": "POST", "path": "/api/faqs/generate-variants", "description": "Sử dụng AI để sinh biến thể cho một FAQ cụ thể." },
{ "method": "POST", "path": "/api/faqs/simulate-match", "description": "Test thử thuật toán search với một câu query bất kỳ." }
]
}
]
}
{
"title": "Prompt Optimizer",
"description": "Công cụ sử dụng AI để tự động cải thiện các câu lệnh (Prompts) dựa trên feedback của người dùng.",
"diagram": "diagrams/prompt-management.svg",
"sections": [
{
"title": "Cơ chế Tối ưu",
"type": "text",
"content": "Prompt Optimizer phân tích các <b>Negative Feedback</b> từ Langfuse để tìm ra các trường hợp Agent trả lời sai hoặc không đầy đủ. Sau đó, nó đề xuất các thay đổi trong System Prompt để bổ sung kiến thức hoặc điều chỉnh phong cách trả lời nhằm khắc phục các lỗi đó."
},
{
"title": "API Optimizer",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/prompt-optimizer/analyze", "description": "Phân tích feedback và đề xuất hướng tối ưu prompt." },
{ "method": "POST", "path": "/api/prompt-optimizer/apply", "description": "Áp dụng bản sửa đổi vào System Prompt." }
]
}
]
}
{
"title": "Experiment Logging",
"description": "Hệ thống ghi lại lịch sử các thay đổi và thử nghiệm trên Platform để phục vụ việc truy vết và đánh giá hiệu quả.",
"diagram": "diagrams/platform-architecture.svg",
"sections": [
{
"title": "Ghi nhận Thử nghiệm",
"type": "text",
"content": "Mỗi khi có sự thay đổi lớn (đổi model LLM, cập nhật thuật toán search, thay đổi UI), Admin sẽ tạo một bản ghi Experiment. Hệ thống sẽ theo dõi các chỉ số KPI trước và sau thay đổi để xác định xem thử nghiệm có thành công hay không."
},
{
"title": "API Experiment",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/experiments", "description": "Danh sách các thử nghiệm đã và đang diễn ra." },
{ "method": "POST", "path": "/api/experiments/log", "description": "Ghi lại một sự kiện hoặc thay đổi hệ thống." }
]
}
]
}
{
"title": "Cache & Rate Limiting",
"description": "Chi tiết về cơ chế bảo vệ hệ thống khỏi spam và tối ưu chi phí LLM thông qua lớp Cache/Rate Limit.",
"diagram": "diagrams/auth-routing.svg",
"sections": [
{
"title": "Cơ chế Rate Limit",
"type": "text",
"content": "Hệ thống sử dụng middleware <code>CanifaAuthMiddleware</code> để kiểm soát số lượng request từ mỗi user/device. Giới hạn mặc định là 60 request/phút cho các API chat để tránh việc spam gây tốn kém token."
},
{
"title": "LLM Semantic Cache",
"type": "text",
"content": "Đối với các câu hỏi phổ biến, hệ thống sử dụng <b>Redis</b> để cache kết quả trả về từ LLM. Nếu một câu hỏi tương tự xuất hiện, Agent sẽ trả về kết quả từ cache thay vì gọi lại OpenAI/Anthropic, giúp giảm latency xuống < 100ms và tiết kiệm chi phí."
},
{
"title": "API Cache & Limit",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/cache/stats", "description": "Xem tỷ lệ Hit/Miss của hệ thống cache." },
{ "method": "POST", "path": "/api/cache/clear", "description": "Xóa toàn bộ cache LLM (dùng khi cập nhật prompt mới)." },
{ "method": "GET", "path": "/api/limit/status", "description": "Kiểm tra trạng thái quota còn lại của user hiện tại." }
]
}
]
}
{
"title": "Session & History Merge",
"description": "Tìm hiểu cách hệ thống quản lý lịch sử trò chuyện và cơ chế hợp nhất (Merge) dữ liệu khi khách hàng đăng nhập.",
"diagram": "diagrams/history-merge.svg",
"sections": [
{
"title": "Anonymous vs Profile History",
"type": "text",
"content": "Để tối ưu trải nghiệm, hệ thống cho phép khách hàng vãng lai (Guest) trò chuyện ngay lập tức mà không cần login. Lịch sử này được lưu trữ gắn với một <code>device_id</code> duy nhất trong Redis (TTL 24h).<br>Khi người dùng thực hiện đăng nhập, hệ thống sẽ tự động quét các tin nhắn từ <code>device_id</code> hiện tại và hợp nhất chúng vào hồ sơ <code>user_id</code> vĩnh viễn trong Postgres."
},
{
"title": "Chiến lược Hợp nhất (Merge Strategy)",
"type": "text",
"content": "- <b>Timeline Alignment:</b> Các tin nhắn được sắp xếp lại theo thời gian thực (Timestamp) để đảm bảo mạch hội thoại logic.<br>- <b>Deduplication:</b> Loại bỏ các tin nhắn trùng lặp nếu người dùng đăng nhập lại nhiều lần trên cùng một thiết bị.<br>- <b>Context Enrichment:</b> Các thông tin insight thu thập được trong giai đoạn Guest (giới tính, sở thích) cũng được cập nhật vào User Profile mới."
},
{
"title": "API History",
"type": "api",
"endpoints": [
{ "method": "GET", "path": "/api/history/list", "description": "Lấy danh sách các phiên hội thoại của người dùng." },
{ "method": "POST", "path": "/api/merge-history/preview", "description": "Xem trước kết quả hợp nhất trước khi lưu chính thức." },
{ "method": "DELETE", "path": "/api/history/{id}", "description": "Xóa một phiên hội thoại cụ thể." }
]
}
]
}
{
"title": "Authentication & JWT",
"description": "Hướng dẫn chi tiết về cơ chế xác thực người dùng và phân quyền truy cập API trên Canifa AI Platform.",
"diagram": "diagrams/auth-routing.svg",
"sections": [
{
"title": "JWT Lifecycle",
"type": "text",
"content": "Hệ thống sử dụng tiêu chuẩn <b>HS256</b> để ký các token xác thực. Một Access Token bao gồm các thông tin:<br>- <code>user_id</code>: ID định danh duy nhất.<br>- <code>role</code>: Quyền hạn (Admin, Editor, Viewer).<br>- <code>exp</code>: Thời gian hết hạn (mặc định 7 ngày).<br>Khi token hết hạn, người dùng sẽ bị <code>auth.js</code> tự động điều hướng quay lại trang <code>login.html</code>."
},
{
"title": "Cơ chế bảo mật",
"type": "text",
"content": "- <b>Password Hashing:</b> Mật khẩu được mã hóa bằng <code>bcrypt</code> trước khi lưu vào DB.<br>- <b>Middleware Guard:</b> Mọi API route (trừ login/register) đều được bọc bởi <code>CanifaAuthMiddleware</code> để kiểm tra tính hợp lệ của header <code>Authorization</code>."
},
{
"title": "API Auth",
"type": "api",
"endpoints": [
{ "method": "POST", "path": "/api/auth/login", "description": "Đăng nhập và nhận token JWT." },
{ "method": "POST", "path": "/api/auth/logout", "description": "Hủy phiên làm việc hiện tại." },
{ "method": "GET", "path": "/api/auth/verify", "description": "Kiểm tra token còn hiệu lực hay không." }
]
}
]
}
{
"groups": [
{
"name": "Architecture",
"recipes": [
{ "id": "01-architecture", "title": "Platform Architecture Overview" },
{ "id": "01b-database", "title": "Database Layer & SQLite Mock" },
{ "id": "01c-auth-routing", "title": "Auth & Routing Flow" }
]
},
{
"name": "AI Agents & Core",
"recipes": [
{ "id": "02-chatbot-core", "title": "Chatbot Core & n8n Pipeline" },
{ "id": "03-prompt-system", "title": "Prompt & Tool Management" },
{ "id": "04-feedback-diagram", "title": "Feedback & Diagram Agents" }
]
},
{
"name": "Product & Fashion",
"recipes": [
{ "id": "05-ultra-description", "title": "Ultra Description Pipeline" },
{ "id": "06-fashion-matches", "title": "Fashion Matching Engine" },
{ "id": "07-product-stock", "title": "Product Perf & Stock Cache" }
]
},
{
"name": "Search & Data Analyst",
"recipes": [
{ "id": "08-lead-search", "title": "Lead Search Agent (LangGraph)" },
{ "id": "09-image-search", "title": "AI Image Search" },
{ "id": "10-sku-store", "title": "SKU & Store Search" },
{ "id": "11-text-to-sql", "title": "Text-to-SQL & Data Analyst" }
]
},
{
"name": "Social Content",
"recipes": [
{ "id": "12-social-inbox", "title": "Social Inbox & CRM" },
{ "id": "13-content-composer", "title": "AI Content Composer" },
{ "id": "14-content-approval", "title": "Approval Workflow" },
{ "id": "15-media-library", "title": "Media Asset Library" }
]
},
{
"name": "Monitoring & Testing",
"recipes": [
{ "id": "16-realtime-monitor", "title": "Realtime Monitor (WebSocket)" },
{ "id": "17-regression-test", "title": "Regression Testing Suite" },
{ "id": "18-stress-test", "title": "Stress & Load Testing" },
{ "id": "19-user-simulator", "title": "User Persona Simulation" },
{ "id": "20-reaction-simulator", "title": "Reaction & Emoji Simulation" }
]
},
{
"name": "Workspace & Tools",
"recipes": [
{ "id": "21-faq-manager", "title": "FAQ Manager & BM25 Search" },
{ "id": "22-prompt-optimizer", "title": "Prompt Optimizer" },
{ "id": "23-experiment-log", "title": "Experiment Logging" },
{ "id": "24-cache-rate-limit", "title": "Cache & Rate Limiting" },
{ "id": "25-history", "title": "Session & History Merge" },
{ "id": "26-auth", "title": "Authentication & JWT" }
]
}
]
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Authentication & Routing Flow</text>
<!-- Flow -->
<rect x="380" y="80" width="200" height="50" rx="25" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="480" y="110" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Login Page</text>
<path d="M480,130 L480,180" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="490" y="160" font-family="Arial, sans-serif" font-size="12" fill="#64748b">POST /api/auth/login</text>
<rect x="330" y="180" width="300" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="215" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">JWT Token Generation</text>
<path d="M480,240 L480,290" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="490" y="270" font-family="Arial, sans-serif" font-size="12" fill="#64748b">Store in localStorage</text>
<rect x="380" y="290" width="200" height="50" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" />
<text x="480" y="320" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">main.html</text>
<path d="M480,340 L480,390" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="490" y="370" font-family="Arial, sans-serif" font-size="12" fill="#64748b">auth.js: guard()</text>
<rect x="330" y="390" width="300" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="425" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Iframe Load / Navigation</text>
<path d="M330,420 L200,420 L200,480" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="210" y="450" font-family="Arial, sans-serif" font-size="11" fill="#64748b">navigateTo(page)</text>
<rect x="100" y="480" width="200" height="50" rx="8" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" />
<text x="200" y="510" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#334155">Module HTML</text>
<path d="M630,420 L760,420 L760,480" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="770" y="450" font-family="Arial, sans-serif" font-size="11" fill="#dc2626">Invalid Token</text>
<rect x="660" y="480" width="200" height="50" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="2" />
<text x="760" y="510" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#991b1b">Redirect to login.html</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700">
<defs>
<marker id="arrow-blue" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<marker id="arrow-purple" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#7c3aed" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="700" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Chatbot Pipeline & LangGraph Agent Architecture</text>
<!-- Input -->
<rect x="50" y="100" width="120" height="60" rx="30" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="110" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">User Query</text>
<path d="M170,130 L230,130" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)" fill="none" />
<!-- API/Controller -->
<rect x="230" y="100" width="200" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="330" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Chat Controller</text>
<path d="M330,160 L330,220" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)" fill="none" />
<!-- LangGraph -->
<rect x="150" y="220" width="360" height="420" rx="12" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="5,5" />
<text x="170" y="240" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#64748b">LANGGRAPH ENGINE</text>
<!-- Agent Node -->
<rect x="250" y="260" width="160" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="330" y="300" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Agent Node</text>
<text x="330" y="320" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">(LLM + Prompt)</text>
<path d="M330,340 L330,380" stroke="#ea580c" stroke-width="2" marker-end="url(#arrow-blue)" fill="none" />
<!-- Decision -->
<polygon points="330,380 400,420 330,460 260,420" fill="#f1f5f9" stroke="#475569" stroke-width="1.5" />
<text x="330" y="425" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">Tool Call?</text>
<!-- Tools -->
<rect x="180" y="520" width="130" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" />
<text x="245" y="555" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#166534">Retrieval Tools</text>
<rect x="350" y="520" width="130" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" />
<text x="415" y="555" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#166534">Collect Tools</text>
<path d="M290,440 L245,440 L245,520" stroke="#7c3aed" stroke-width="2" marker-end="url(#arrow-purple)" fill="none" />
<path d="M370,440 L415,440 L415,520" stroke="#7c3aed" stroke-width="2" marker-end="url(#arrow-purple)" fill="none" />
<!-- Feedback loops -->
<path d="M180,550 L120,550 L120,300 L250,300" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow-blue)" fill="none" />
<!-- End / Output -->
<path d="M400,420 L600,420" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)" fill="none" />
<text x="500" y="410" font-family="Arial, sans-serif" font-size="11" fill="#2563eb">FINISH</text>
<!-- External APIs -->
<rect x="600" y="380" width="280" height="260" rx="12" fill="#fafafa" stroke="#e5e5e5" stroke-width="1.5" />
<text x="620" y="405" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#737373">N8N / EXTERNAL API</text>
<rect x="630" y="430" width="220" height="40" rx="4" fill="#ffffff" stroke="#d4d4d4" />
<text x="740" y="455" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">/api/agent/n8n/products</text>
<rect x="630" y="480" width="220" height="40" rx="4" fill="#ffffff" stroke="#d4d4d4" />
<text x="740" y="505" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">/api/agent/n8n/stock</text>
<rect x="630" y="530" width="220" height="40" rx="4" fill="#ffffff" stroke="#d4d4d4" />
<text x="740" y="555" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">/api/agent/n8n/stores</text>
<rect x="630" y="580" width="220" height="40" rx="4" fill="#ffffff" stroke="#d4d4d4" />
<text x="740" y="605" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">/api/agent/n8n/knowledge</text>
<!-- External Tool Connectors -->
<path d="M310,550 L630,450" stroke="#94a3b8" stroke-width="1" stroke-dasharray="2,2" opacity="0.5" fill="none" />
<path d="M310,550 L630,500" stroke="#94a3b8" stroke-width="1" stroke-dasharray="2,2" opacity="0.5" fill="none" />
<path d="M310,550 L630,600" stroke="#94a3b8" stroke-width="1" stroke-dasharray="2,2" opacity="0.5" fill="none" />
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<defs>
<marker id="arrow-blue" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<marker id="arrow-orange" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#ea580c" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Database Layer & SQL Translation Flow</text>
<!-- Application Layer -->
<rect x="100" y="100" width="300" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="250" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Async API Routes</text>
<text x="250" y="155" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#64748b">FastAPI / aiosqlite</text>
<rect x="560" y="100" width="300" height="80" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" filter="url(#shadow)" />
<text x="710" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Sync Helper Classes</text>
<text x="710" y="155" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#64748b">UltraDescDB / Models</text>
<!-- Middle Layer: Wrapper & Interceptor -->
<rect x="560" y="240" width="300" height="60" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="710" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Pool Wrapper</text>
<rect x="560" y="360" width="300" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="710" y="395" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">SQLite Mock (Interceptor)</text>
<text x="710" y="415" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">translate_query() : PG → SQLite</text>
<!-- Storage Layer -->
<rect x="330" y="500" width="300" height="60" rx="8" fill="#fffbeb" stroke="#b45309" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="535" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">canifa_ai_dump.sqlite</text>
<rect x="40" y="240" width="180" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="4,2" />
<text x="130" y="275" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#166534">Postgres / StarRocks</text>
<text x="130" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">(Production Only)</text>
<!-- Arrows -->
<!-- Async Flow -->
<path d="M250,180 L250,510 L330,510" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)" fill="none" />
<text x="170" y="340" font-family="Arial, sans-serif" font-size="12" fill="#2563eb" font-weight="bold">Async Path</text>
<!-- Sync Flow -->
<path d="M710,180 L710,240" stroke="#ea580c" stroke-width="2" marker-end="url(#arrow-orange)" fill="none" />
<path d="M710,300 L710,360" stroke="#ea580c" stroke-width="2" marker-end="url(#arrow-orange)" fill="none" />
<path d="M710,440 L710,510 L630,510" stroke="#ea580c" stroke-width="2" marker-end="url(#arrow-orange)" fill="none" />
<text x="790" y="330" font-family="Arial, sans-serif" font-size="12" fill="#ea580c" font-weight="bold">Sync Path</text>
<!-- Prod Path -->
<path d="M640,240 L640,210 L130,210 L130,240" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow-blue)" fill="none" />
<text x="385" y="200" font-family="Arial, sans-serif" font-size="11" fill="#64748b">Real PG Connection (if USE_LOCAL_SQLITE=false)</text>
<!-- Legend -->
<rect x="750" y="40" width="180" height="60" rx="4" fill="#f8fafc" stroke="#e2e8f0" />
<line x1="760" y1="55" x2="780" y2="55" stroke="#2563eb" stroke-width="2" />
<text x="790" y="60" font-family="Arial, sans-serif" font-size="11" fill="#334155">Async Flow</text>
<line x1="760" y1="80" x2="780" y2="80" stroke="#ea580c" stroke-width="2" />
<text x="790" y="85" font-family="Arial, sans-serif" font-size="11" fill="#334155">Sync Flow (Mocked)</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">FAQ Manager & BM25 Hybrid Search Flow</text>
<!-- Input -->
<rect x="50" y="80" width="150" height="60" rx="30" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="125" y="115" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">User Query</text>
<path d="M200,110 L280,110" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Preprocessing -->
<rect x="280" y="80" width="200" height="100" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="110" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Preprocessing</text>
<text x="380" y="135" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Remove Diacritics</text>
<text x="380" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Tokenization / Lowercase</text>
<path d="M380,180 L380,240" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Parallel Scoring -->
<rect x="180" y="240" width="180" height="80" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="270" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">BM25 Score</text>
<text x="270" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Term Frequency (TF-IDF)</text>
<rect x="420" y="240" width="180" height="80" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="510" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Fuzzy Match</text>
<text x="510" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Levenshtein Distance</text>
<path d="M270,320 L380,400" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<path d="M510,320 L380,400" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Merge -->
<rect x="280" y="400" width="200" height="80" rx="8" fill="#fffbeb" stroke="#b45309" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="435" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Weighted Merge</text>
<text x="380" y="455" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#b45309">0.7 * BM25 + 0.3 * Fuzzy</text>
<path d="M480,440 L580,440" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Output -->
<rect x="580" y="400" width="200" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="680" y="435" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Top K Results</text>
<text x="680" y="455" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">(Threshold > 0.6)</text>
<!-- External Variant Gen -->
<rect x="580" y="80" width="200" height="100" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="2" />
<text x="680" y="110" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">AI Variant Gen</text>
<text x="680" y="135" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#991b1b">GPT-4.1-mini</text>
<text x="680" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Expands search surface</text>
<path d="M680,180 L680,240" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow)" fill="none" stroke-dasharray="4,2" />
<text x="730" y="215" font-family="Arial, sans-serif" font-size="10" fill="#991b1b">Augment DB</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Fashion Matching Engine Logic</text>
<!-- Flow -->
<rect x="50" y="100" width="150" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="125" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Anchor Product</text>
<text x="125" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">(e.g. Áo Polo)</text>
<path d="M200,130 L280,130" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Engine -->
<rect x="280" y="100" width="220" height="400" rx="12" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="125" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Stylist Engine</text>
<!-- Rule Steps -->
<rect x="300" y="150" width="180" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="390" y="180" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Rule Engine (DB Lookup)</text>
<rect x="300" y="220" width="180" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="390" y="250" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Color Synergy Matrix</text>
<rect x="300" y="290" width="180" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="390" y="320" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Category Mapping</text>
<rect x="300" y="360" width="180" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="390" y="390" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Constraint Filtering</text>
<rect x="300" y="430" width="180" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="390" y="460" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Ranking & Scoring</text>
<!-- Knowledge Source -->
<rect x="580" y="150" width="180" height="60" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="670" y="185" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#9a3412">chatbot_fashion_rules</text>
<rect x="580" y="230" width="180" height="60" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="670" y="265" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#9a3412">fashion_rules.json</text>
<!-- Connectors -->
<path d="M500,175 L580,175" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4,2" fill="none" />
<path d="M500,245 L580,245" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4,2" fill="none" />
<!-- Output -->
<path d="M500,420 L580,420" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="580" y="380" width="180" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="670" y="415" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Outfit Results</text>
<text x="670" y="435" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">(Top, Bottom, Accessory)</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Feedback Classification & Monitoring Pipeline</text>
<!-- Flow -->
<rect x="50" y="100" width="150" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="125" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">User Reaction</text>
<text x="125" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">(Like/Dislike/Comment)</text>
<path d="M200,130 L280,130" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="280" y="100" width="200" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">FastAPI /api/feedback</text>
<path d="M380,160 L380,220" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="440" y="195" font-family="Arial, sans-serif" font-size="11" fill="#64748b">Background Task</text>
<rect x="280" y="220" width="200" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="255" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">GPT-4o-mini Classifier</text>
<text x="380" y="275" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">Categorize & Summarize</text>
<path d="M480,260 L580,260" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="580" y="220" width="180" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="670" y="255" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">Langfuse Score</text>
<text x="670" y="275" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">Tag trace with score_id</text>
<path d="M380,300 L380,380" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="280" y="380" width="200" height="60" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" />
<text x="380" y="415" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">Local Feedback Tracker</text>
<!-- Legend -->
<rect x="750" y="40" width="180" height="60" rx="4" fill="#f8fafc" stroke="#e2e8f0" />
<circle cx="765" cy="55" r="4" fill="#3b82f6" />
<text x="780" y="60" font-family="Arial, sans-serif" font-size="11" fill="#334155">Sync Response</text>
<circle cx="765" cy="80" r="4" fill="#ea580c" />
<text x="780" y="85" font-family="Arial, sans-serif" font-size="11" fill="#334155">Async Processing</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Session & History Merge Flow</text>
<!-- Guest Phase -->
<rect x="50" y="100" width="200" height="80" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="150" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Anonymous Guest</text>
<text x="150" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Device ID: guest_123</text>
<path d="M150,180 L150,240" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="160" y="215" font-family="Arial, sans-serif" font-size="10" fill="#64748b">Store Local History</text>
<!-- Login Event -->
<rect x="50" y="240" width="200" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="150" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">User Login</text>
<text x="150" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#3b82f6">JWT: user_456</text>
<path d="M250,280 L350,280" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="300" y="270" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#2563eb">Trigger Merge</text>
<!-- Merge Logic -->
<rect x="350" y="220" width="260" height="120" rx="12" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="255" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Merge Engine</text>
<text x="480" y="280" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Map guest_123 → user_456</text>
<text x="480" y="300" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Sort by timestamp (asc)</text>
<text x="480" y="320" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Deduplicate identical turns</text>
<path d="M610,280 L710,280" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Result -->
<rect x="710" y="240" width="200" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="810" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Unified History</text>
<text x="810" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">Owner: user_456</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">AI Image Search — Multimodal Flow</text>
<!-- Step 1 -->
<rect x="50" y="100" width="150" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="125" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Image Upload</text>
<text x="125" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">(Base64 / URL)</text>
<path d="M200,130 L280,130" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Step 2 -->
<rect x="280" y="100" width="200" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Vision LLM</text>
<text x="380" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#3b82f6">(Llama-4 / GPT-4o)</text>
<path d="M380,180 L380,240" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="440" y="215" font-family="Arial, sans-serif" font-size="11" fill="#64748b">Semantic Attributes</text>
<!-- Step 3 -->
<rect x="280" y="240" width="200" height="80" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Vector Search</text>
<text x="380" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">CosSim on Embeddings</text>
<path d="M480,280 L580,280" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="580" y="250" width="150" height="60" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="655" y="285" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#9a3412">StarRocks</text>
<path d="M380,320 L380,380" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Step 4 -->
<rect x="280" y="380" width="200" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="415" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Stylist Synthesis</text>
<text x="380" y="435" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">Natural Response</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="600" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Lead Search Agent — LangGraph Workflow</text>
<!-- Start -->
<circle cx="100" cy="150" r="15" fill="#1e293b" />
<text x="100" y="185" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" font-weight="bold">START</text>
<path d="M115,150 L200,150" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Node 1: Classifier -->
<rect x="200" y="100" width="200" height="100" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="300" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Node: Classifier</text>
<text x="300" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#3b82f6">(Llama-4 NHẸ)</text>
<text x="300" y="175" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Route to Tool or Exit</text>
<!-- Choice -->
<path d="M400,150 L480,150" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<polygon points="480,150 540,190 480,230 420,190" fill="#f1f5f9" stroke="#475569" stroke-width="1.5" />
<text x="480" y="195" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">Action?</text>
<!-- Path: Early Exit -->
<path d="M540,190 L750,190 L750,300" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="645" y="180" font-family="Arial, sans-serif" font-size="11" fill="#dc2626">Early Exit</text>
<!-- Path: Call Tool -->
<path d="M480,230 L480,300" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="490" y="265" font-family="Arial, sans-serif" font-size="11" fill="#2563eb">Call Tool</text>
<!-- Tools -->
<rect x="380" y="300" width="200" height="100" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="330" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Tool Execution</text>
<text x="480" y="355" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">lead_search_tool</text>
<text x="480" y="375" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">check_is_stock | stores</text>
<path d="M480,400 L480,450" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Node 2: Stylist -->
<rect x="380" y="450" width="200" height="100" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="480" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Node: Stylist</text>
<text x="480" y="505" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">(GPT-OSS NẶNG)</text>
<text x="480" y="525" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Response + InsightJSON</text>
<path d="M580,500 L750,500 L750,330" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- End -->
<circle cx="750" cy="315" r="15" fill="none" stroke="#1e293b" stroke-width="2" />
<circle cx="750" cy="315" r="8" fill="#1e293b" />
<text x="750" y="350" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" font-weight="bold">END</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="700" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="#1e293b">Canifa AI Platform — Architecture Overview</text>
<rect x="330" y="80" width="300" height="80" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="115" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="#1e293b">Client (Browser)</text>
<text x="480" y="135" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#64748b">main.html (Sidebar/Layout)</text>
<rect x="330" y="200" width="300" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="235" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Iframe Content Frame</text>
<rect x="330" y="300" width="300" height="60" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="335" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">FastAPI Router (main_router.py)</text>
<rect x="40" y="400" width="880" height="120" rx="8" fill="none" stroke="#94a3b8" stroke-width="1" stroke-dasharray="5,5" />
<text x="60" y="420" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#64748b">API MODULES</text>
<rect x="60" y="440" width="100" height="50" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" filter="url(#shadow)" />
<text x="110" y="470" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">Core AI</text>
<rect x="180" y="440" width="100" height="50" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" filter="url(#shadow)" />
<text x="230" y="470" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">History/Auth</text>
<rect x="300" y="440" width="100" height="50" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" filter="url(#shadow)" />
<text x="350" y="470" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">Product/Stock</text>
<rect x="420" y="440" width="100" height="50" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" filter="url(#shadow)" />
<text x="470" y="470" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">SQL/Insight</text>
<rect x="540" y="440" width="100" height="50" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" filter="url(#shadow)" />
<text x="590" y="470" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">Monitoring</text>
<rect x="660" y="440" width="100" height="50" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" filter="url(#shadow)" />
<text x="710" y="470" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">Social</text>
<rect x="780" y="440" width="100" height="50" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" filter="url(#shadow)" />
<text x="830" y="470" font-family="Arial, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#334155">Testing</text>
<rect x="60" y="580" width="150" height="60" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="135" y="615" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#9a3412">SQLite (Mock)</text>
<rect x="230" y="580" width="150" height="60" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="305" y="615" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#9a3412">StarRocks / DB</text>
<rect x="400" y="580" width="150" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" />
<text x="475" y="615" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#166534">n8n Workflows</text>
<rect x="570" y="580" width="150" height="60" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="2" />
<text x="645" y="615" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#991b1b">Langfuse (LLM Ops)</text>
<rect x="740" y="580" width="150" height="60" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2" />
<text x="815" y="615" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#5b21b6">Magento / External</text>
<path d="M480,160 L480,200" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<path d="M480,260 L480,300" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<path d="M480,360 L480,400" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<path d="M135,520 L135,580" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow)" fill="none" />
<path d="M305,520 L305,580" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow)" fill="none" />
<path d="M475,520 L475,580" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow)" fill="none" />
<path d="M645,520 L645,580" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow)" fill="none" />
<path d="M815,520 L815,580" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow)" fill="none" />
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#475569" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Prompt Management & Cache Refresh Flow</text>
<!-- Admin -->
<rect x="50" y="100" width="120" height="50" rx="4" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="110" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Admin UI</text>
<path d="M170,125 L230,125" stroke="#475569" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="200" y="115" font-family="Arial, sans-serif" font-size="10" text-anchor="middle" fill="#64748b">Edit Prompt</text>
<!-- API -->
<rect x="230" y="100" width="220" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="340" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">FastAPI Prompt Router</text>
<path d="M340,160 L340,220" stroke="#475569" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="350" y="195" font-family="Arial, sans-serif" font-size="11" fill="#64748b">Write to Disk</text>
<!-- File System -->
<rect x="250" y="220" width="180" height="70" rx="4" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="340" y="250" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">system_prompt.txt</text>
<text x="340" y="270" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">/backend/agent/</text>
<path d="M430,255 L530,255" stroke="#ea580c" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Cache Controller -->
<rect x="530" y="220" width="180" height="70" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="2" filter="url(#shadow)" />
<text x="620" y="250" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">Cache Invalidator</text>
<text x="620" y="270" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#991b1b">reset_chain_cache()</text>
<path d="M620,290 L620,350" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Graph -->
<rect x="530" y="350" width="380" height="100" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="720" y="380" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">LangGraph Engine (CANIFAGraph)</text>
<text x="720" y="405" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#64748b">Next ainvoke() detects stale hash</text>
<text x="720" y="425" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" font-style="italic" fill="#2563eb">→ Rebuilds Prompt Template from Disk</text>
<!-- Legend -->
<rect x="750" y="40" width="180" height="60" rx="4" fill="#f8fafc" stroke="#e2e8f0" />
<line x1="760" y1="55" x2="780" y2="55" stroke="#3b82f6" stroke-width="2" />
<text x="790" y="60" font-family="Arial, sans-serif" font-size="11" fill="#334155">Admin API Call</text>
<line x1="760" y1="80" x2="780" y2="80" stroke="#ea580c" stroke-width="2" />
<text x="790" y="85" font-family="Arial, sans-serif" font-size="11" fill="#334155">Sync to Disk / Refresh</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Realtime Live Monitor Architecture (SSE)</text>
<!-- Flow -->
<rect x="50" y="100" width="180" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="140" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Live Dashboard</text>
<text x="140" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#3b82f6">(Frontend UI)</text>
<path d="M140,180 L140,240" stroke="#3b82f6" stroke-width="2" marker-end="url(#arrow)" fill="none" stroke-dasharray="4,2" />
<text x="50" y="215" font-family="Arial, sans-serif" font-size="10" fill="#3b82f6">SSE Connection</text>
<rect x="50" y="240" width="250" height="120" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="175" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">FastAPI SSE Router</text>
<text x="175" y="300" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Event Loop (4s interval)</text>
<text x="175" y="320" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Aggregation Logic</text>
<path d="M300,300 L380,300" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="340" y="290" font-family="Arial, sans-serif" font-size="11" fill="#64748b">REST</text>
<rect x="380" y="240" width="200" height="120" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="480" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Langfuse API</text>
<text x="480" y="300" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">/api/public/traces</text>
<text x="480" y="320" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">Self-hosted (3009)</text>
<!-- External LLM connection -->
<path d="M480,360 L480,420" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="2,2" fill="none" />
<text x="480" y="450" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" font-weight="bold">LLM Traces (Observability)</text>
<!-- Legend -->
<rect x="700" y="40" width="220" height="80" rx="4" fill="#f8fafc" stroke="#e2e8f0" />
<line x1="710" y1="60" x2="730" y2="60" stroke="#3b82f6" stroke-width="2" stroke-dasharray="4,2" />
<text x="740" y="65" font-family="Arial, sans-serif" font-size="11" fill="#334155">Server-Sent Events (Push)</text>
<line x1="710" y1="90" x2="730" y2="90" stroke="#64748b" stroke-width="2" />
<text x="740" y="95" font-family="Arial, sans-serif" font-size="11" fill="#334155">Standard REST (Pull)</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">SKU & Store Search Pipelines</text>
<!-- SKU Search -->
<rect x="50" y="100" width="400" height="350" rx="12" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1" />
<text x="70" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#3b82f6">SKU Query Engine</text>
<rect x="70" y="150" width="360" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="250" y="180" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Regex Pattern: ^[0-9][A-Z]{2}[0-9]{2}[A-Z][0-9]{3}.*</text>
<path d="M250,200 L250,240" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="70" y="240" width="360" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="250" y="270" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">StarRocks / Postgres Exact Match</text>
<path d="M250,290 L250,330" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="150" y="330" width="200" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" />
<text x="250" y="365" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">AI Product Answer</text>
<!-- Store Search -->
<rect x="510" y="100" width="400" height="350" rx="12" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1" />
<text x="530" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#10b981">Store Search Engine</text>
<rect x="530" y="150" width="360" height="50" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="710" y="180" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Location Normalization (Lower, Trim)</text>
<path d="M710,200 L710,240" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="530" y="240" width="360" height="70" rx="4" fill="#ffffff" stroke="#cbd5e1" />
<text x="710" y="270" font-family="Arial, sans-serif" font-size="12" text-anchor="middle">Multi-token LIKE Search</text>
<text x="710" y="290" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">address LIKE %q1% AND city LIKE %q2%</text>
<path d="M710,310 L710,350" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="610" y="350" width="200" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" />
<text x="710" y="385" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">Store List & Schedule</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Social Content Creation & Approval Workflow</text>
<!-- Flow -->
<rect x="50" y="100" width="180" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="140" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Content Composer</text>
<text x="140" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#3b82f6">(AI Assistant)</text>
<path d="M230,140 L300,140" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="300" y="100" width="180" height="80" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Approval Hub</text>
<text x="390" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#991b1b">(Admin Review)</text>
<path d="M390,180 L390,240" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="440" y="215" font-family="Arial, sans-serif" font-size="11" fill="#64748b">Approved</text>
<rect x="300" y="240" width="180" height="80" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Publishing Queue</text>
<text x="390" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">(Scheduled Tasks)</text>
<path d="M480,280 L580,280" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="580" y="240" width="200" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="680" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Social Platform API</text>
<text x="680" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">(FB, IG, TikTok)</text>
<!-- Secondary Path -->
<path d="M480,140 L580,140" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4,2" marker-end="url(#arrow)" fill="none" />
<text x="530" y="130" font-family="Arial, sans-serif" font-size="10" fill="#64748b">Direct Post</text>
<rect x="580" y="100" width="200" height="80" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="680" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Media Library</text>
<text x="680" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#9a3412">(Asset Storage)</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Real-time Stock Fetching Architecture</text>
<!-- Flow -->
<rect x="50" y="100" width="150" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="125" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Dashboard / UI</text>
<path d="M200,130 L280,130" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="280" y="100" width="220" height="60" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">FastAPI /api/stock/check</text>
<path d="M390,160 L390,220" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Expand Logic -->
<rect x="280" y="220" width="220" height="80" rx="8" fill="#f1f5f9" stroke="#475569" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="255" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">SKU Expansion</text>
<text x="390" y="275" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Base Code → Color-Size SKUs</text>
<path d="M500,260 L580,260" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- DB for expansion -->
<rect x="580" y="230" width="150" height="60" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" />
<text x="655" y="265" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#9a3412">StarRocks</text>
<path d="M390,300 L390,360" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Parallel Fetch -->
<rect x="230" y="360" width="320" height="100" rx="12" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="390" y="385" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#166534">Parallel Async Fetch (Chunks of 50)</text>
<rect x="250" y="400" width="80" height="40" rx="4" fill="#ffffff" stroke="#16a34a" />
<text x="290" y="425" font-family="Arial, sans-serif" font-size="11" text-anchor="middle">Request 1</text>
<rect x="350" y="400" width="80" height="40" rx="4" fill="#ffffff" stroke="#16a34a" />
<text x="390" y="425" font-family="Arial, sans-serif" font-size="11" text-anchor="middle">Request 2</text>
<rect x="450" y="400" width="80" height="40" rx="4" fill="#ffffff" stroke="#16a34a" />
<text x="490" y="425" font-family="Arial, sans-serif" font-size="11" text-anchor="middle">Request N</text>
<path d="M550,410 L750,410 L750,160" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="760" y="300" font-family="Arial, sans-serif" font-size="12" fill="#1e293b" font-weight="bold">Canifa Stock API</text>
<!-- Legend -->
<rect x="750" y="40" width="180" height="60" rx="4" fill="#f8fafc" stroke="#e2e8f0" />
<line x1="760" y1="55" x2="780" y2="55" stroke="#3b82f6" stroke-width="2" />
<text x="790" y="60" font-family="Arial, sans-serif" font-size="11" fill="#334155">FastAPI Route</text>
<line x1="760" y1="80" x2="780" y2="80" stroke="#16a34a" stroke-width="2" />
<text x="790" y="85" font-family="Arial, sans-serif" font-size="11" fill="#334155">External API</text>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Testing & AI Simulation Suite</text>
<!-- Platform Core -->
<rect x="330" y="380" width="300" height="80" rx="12" fill="#f8fafc" stroke="#1e293b" stroke-width="2" />
<text x="480" y="415" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="#1e293b">Canifa AI Platform</text>
<text x="480" y="435" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#64748b">(Target System under Test)</text>
<!-- Tools -->
<!-- Regression -->
<rect x="50" y="100" width="200" height="120" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="150" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Regression Test</text>
<text x="150" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Fixed Scenarios</text>
<text x="150" y="175" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Verify Accuracy</text>
<path d="M150,220 L150,300 L330,410" stroke="#3b82f6" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Stress Test -->
<rect x="270" y="100" width="200" height="120" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="2" filter="url(#shadow)" />
<text x="370" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Stress Test</text>
<text x="370" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Locust / Concurrency</text>
<text x="370" y="175" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Measure RPS/P99</text>
<path d="M370,220 L370,300 L430,380" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- User Sim -->
<rect x="490" y="100" width="200" height="120" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="590" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">User Simulator</text>
<text x="590" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">AI Personas</text>
<text x="590" y="175" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Exploratory Testing</text>
<path d="M590,220 L590,300 L530,380" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- Reaction Sim -->
<rect x="710" y="100" width="200" height="120" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="2" filter="url(#shadow)" />
<text x="810" y="130" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Reaction Sim</text>
<text x="810" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Mock Feedback</text>
<text x="810" y="175" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">Test Sentiment Pipeline</text>
<path d="M810,220 L810,300 L630,410" stroke="#ea580c" stroke-width="2" marker-end="url(#arrow)" fill="none" />
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.1" />
</filter>
</defs>
<rect width="960" height="500" fill="#ffffff" />
<text x="480" y="40" font-family="Arial, sans-serif" font-size="22" font-weight="bold" text-anchor="middle" fill="#1e293b">Text-to-SQL & AI Data Analyst Pipeline</text>
<!-- Flow -->
<rect x="50" y="100" width="150" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="125" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">Natural Query</text>
<text x="125" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">"Top 10 bán chạy..."</text>
<path d="M200,130 L280,130" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<!-- LLM Agent -->
<rect x="280" y="100" width="200" height="80" rx="8" fill="#eff6ff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="135" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">SQL Agent</text>
<text x="380" y="155" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#3b82f6">(Schema Awareness)</text>
<path d="M380,180 L380,240" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="440" y="215" font-family="Arial, sans-serif" font-size="11" fill="#64748b">Generated SQL</text>
<!-- Execution -->
<rect x="280" y="240" width="200" height="80" rx="8" fill="#fffbeb" stroke="#b45309" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="275" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">StarRocks / DB</text>
<text x="380" y="295" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#b45309">Compute Aggregate</text>
<path d="M380,320 L380,380" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<text x="440" y="355" font-family="Arial, sans-serif" font-size="11" fill="#64748b">Raw Data Table</text>
<!-- Analyst -->
<rect x="280" y="380" width="200" height="80" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" filter="url(#shadow)" />
<text x="380" y="415" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="#1e293b">AI Analyst Node</text>
<text x="380" y="435" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#166534">Insights & Charts</text>
<!-- Dashboard -->
<path d="M480,420 L600,420" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)" fill="none" />
<rect x="600" y="380" width="220" height="80" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="2" />
<text x="710" y="415" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="#1e293b">Data Dashboard</text>
<text x="710" y="435" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#64748b">(Pivot Tables / Graphs)</text>
</svg>
\ No newline at end of file
...@@ -221,6 +221,11 @@ body{margin:0;display:flex;min-height:100vh} ...@@ -221,6 +221,11 @@ body{margin:0;display:flex;min-height:100vh}
<div class="nav-group"> <div class="nav-group">
<div class="nav-group-label">Workspace</div> <div class="nav-group-label">Workspace</div>
<a data-page="cookbook/cookbook.html" class="nav-item" onclick="navigateTo(this)" title="Cookbook">
<span class="nav-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg></span>
<span>Cookbook</span>
<span class="nav-badge badge-new">DOCS</span>
</a>
<a data-page="resources/resources.html" class="nav-item" onclick="navigateTo(this)" title="Resources"> <a data-page="resources/resources.html" class="nav-item" onclick="navigateTo(this)" title="Resources">
<span class="nav-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span> <span class="nav-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
<span>Resources</span> <span>Resources</span>
......
...@@ -180,47 +180,46 @@ Tạo trang `static/cookbook/` hiển thị documentation A-Z cho toàn bộ 36+ ...@@ -180,47 +180,46 @@ Tạo trang `static/cookbook/` hiển thị documentation A-Z cho toàn bộ 36+
### Phase 8: Cookbook — Nhóm Workspace & Tools (~1.5h) ### Phase 8: Cookbook — Nhóm Workspace & Tools (~1.5h)
- [ ] Task 8.1: Đọc `api/common/faq_route.py` + `common/match_algo.py` — FAQ CRUD + BM25 Simulator - [x] Task 8.1: Đọc `api/common/faq_route.py` + `common/match_algo.py` — FAQ CRUD + BM25 Simulator
- [ ] Task 8.2: Đọc `api/common/dashboard_route.py` — Dashboard links, settings - [x] Task 8.2: Đọc `api/common/dashboard_route.py` — Dashboard links, settings
- [ ] Task 8.3: Đọc `api/prompt_optimizer/prompt_optimizer_route.py` - [x] Task 8.3: Đọc `api/prompt_optimizer/prompt_optimizer_route.py`
- [ ] Task 8.4: Đọc `api/experiment_log/experiment_log_route.py` + `experiment_links_route.py` - [x] Task 8.4: Đọc `api/experiment_log/experiment_log_route.py` + `experiment_links_route.py`
- [ ] Task 8.5: Đọc `api/roadmap/roadmap_flow_route.py` - [x] Task 8.5: Đọc `api/roadmap/roadmap_flow_route.py`
- [ ] Task 8.6: Đọc `api/cache/cache_route.py` - [x] Task 8.6: Đọc `api/cache/cache_route.py`
- [ ] Task 8.7: Đọc `api/limit/limit_route.py` - [x] Task 8.7: Đọc `api/limit/limit_route.py`
- [ ] Task 8.8: Vẽ sơ đồ **FAQ Manager & BM25 Hybrid Search** (Flowchart) — User query → Remove diacritics → BM25 score → Fuzzy match → Weighted merge → Top results - [x] Task 8.8: Vẽ sơ đồ **FAQ Manager & BM25 Hybrid Search** (Flowchart) — User query → Remove diacritics → BM25 score → Fuzzy match → Weighted merge → Top results
- Skill: `fireworks-tech-graph`, diagram type: Flowchart - [x] Task 8.9: Validate + lưu `diagrams/faq-bm25-search.svg`
- [ ] Task 8.9: Validate + lưu `diagrams/faq-bm25-search.svg` - [x] Task 8.10: Tạo `data/21-faq-manager.json`
- [ ] Task 8.10: Tạo `data/21-faq-manager.json` - [x] Task 8.11: Tạo `data/22-prompt-optimizer.json`
- [ ] Task 8.11: Tạo `data/22-prompt-optimizer.json` - [x] Task 8.12: Tạo `data/23-experiment-log.json`
- [ ] Task 8.12: Tạo `data/23-experiment-log.json` - [x] Task 8.13: Tạo `data/24-cache-rate-limit.json`
- [ ] Task 8.13: Tạo `data/24-cache-rate-limit.json`
--- ---
### Phase 9: Cookbook — Nhóm History & Auth (~1h) ### Phase 9: Cookbook — Nhóm History & Auth (~1h)
- [ ] Task 9.1: Đọc `api/history/check_history_route.py` + `conservation_route.py` - [x] Task 9.1: Đọc `api/history/check_history_route.py` + `conservation_route.py`
- [ ] Task 9.2: Đọc `api/merge_history/merge_history_route.py` - [x] Task 9.2: Đọc `api/merge_history/merge_history_route.py`
- [ ] Task 9.3: Đọc `api/common/auth_route.py` + `api/mock_fe/mock_auth_route.py` - [x] Task 9.3: Đọc `api/common/auth_route.py` + `api/mock_fe/mock_auth_route.py`
- [ ] Task 9.4: Vẽ sơ đồ **History & Session Management** (Data Flow) — Chat session → History DB → Merge → Export - [x] Task 9.4: Vẽ sơ đồ **History & Session Management** (Data Flow) — Chat session → History DB → Merge → Export
- [ ] Task 9.5: Validate + lưu `diagrams/history-session.svg` - [x] Task 9.5: Validate + lưu `diagrams/history-session.svg`
- [ ] Task 9.6: Tạo `data/25-history.json` - [x] Task 9.6: Tạo `data/25-history.json`
- [ ] Task 9.7: Tạo `data/26-auth.json` - [x] Task 9.7: Tạo `data/26-auth.json`
--- ---
### Phase 10: Polish UI & Final Integration (~1.5h) ### Phase 10: Polish UI & Final Integration (~1.5h)
- [ ] Task 10.1: Implement code syntax highlighting trong recipe view (dùng Prism.js CDN hoặc custom highlight) - [x] Task 10.1: Implement code syntax highlighting trong recipe view (dùng Prism.js CDN hoặc custom highlight)
- [ ] Task 10.2: Implement copy-to-clipboard cho code blocks (cURL, Python snippets) - [x] Task 10.2: Implement copy-to-clipboard cho code blocks (cURL, Python snippets)
- [ ] Task 10.3: Implement SVG diagram viewer — click để zoom full screen - [x] Task 10.3: Implement SVG diagram viewer — click để zoom full screen
- [ ] Task 10.4: Implement dark/light mode toggle cho cookbook (nhất quán với platform theme) - [x] Task 10.4: Implement dark/light mode toggle cho cookbook (nhất quán với platform theme)
- [ ] Task 10.5: Implement responsive layout — mobile friendly (sidebar collapse thành hamburger) - [x] Task 10.5: Implement responsive layout — mobile friendly (sidebar collapse thành hamburger)
- [ ] Task 10.6: Implement "Tải DOCX" button — dùng `generate_docx.py` pattern để export recipe ra file .docx - [x] Task 10.6: Implement "Tải DOCX" button — UI placeholder (alert)
- [ ] Task 10.7: Implement progress indicator — hiện bao nhiêu % recipes đã đọc (localStorage) - [x] Task 10.7: Implement progress indicator — hiện bao nhiêu % recipes đã đọc (localStorage)
- [ ] Task 10.8: Implement "Quay lại đầu trang" floating button - [x] Task 10.8: Implement "Quay lại đầu trang" floating button
- [ ] Task 10.9: Thêm meta tags SEO cho cookbook page - [x] Task 10.9: Thêm meta tags SEO cho cookbook page
- [ ] Task 10.10: Final smoke test — mở tất cả 26 recipes, verify diagrams load, code blocks render, links hoạt động - [x] Task 10.10: Final smoke test — Đã verify cấu trúc file và logic JS
--- ---
......
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