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

feat: Memos-style Settings, History Viewer v2, Roadmap page, modular CSS

parent b0bae89f
......@@ -92,7 +92,7 @@ GROUP BY ngay LIMIT 20;
Table: `{LANGFUSE_TABLE}`
{LANGFUSE_SCHEMA}
### 2. sql_langfuse — CleverTap events on StarRocks
### 2. sql_langfuse — CleverTap events on StarRocks
Table: `{CLEVERTAP_TABLE}`
{CLEVERTAP_SCHEMA}
......
......@@ -34,21 +34,35 @@ class ClearHistoryResponse(BaseModel):
@router.get("/api/check-history/{identity_key}", summary="Get Chat History (Check)", response_model=ChatHistoryResponse)
@rate_limit_service.limiter.limit("30/minute")
async def get_chat_history(request: Request, identity_key: str, limit: int | None = 50, before_id: int | None = None):
async def get_chat_history(
request: Request,
identity_key: str,
limit: int | None = 50,
before_id: int | None = None,
date_from: str | None = None,
date_to: str | None = None,
):
"""
Lấy lịch sử chat theo identity_key (có phân trang).
Note: Dùng identity_key từ URL path trực tiếp (không resolve từ middleware).
Chỉ dùng cho show/display lịch sử.
- date_from: YYYY-MM-DD (inclusive)
- date_to: YYYY-MM-DD (inclusive)
"""
try:
if not identity_key or identity_key.strip() == "":
raise HTTPException(status_code=400, detail="identity_key không được rỗng")
logger.info(f"GET History: identity={identity_key} | limit={limit}")
logger.info(f"GET History: identity={identity_key} | limit={limit} | date={date_from}~{date_to}")
manager = await get_conversation_manager()
history = await manager.get_chat_history(identity_key, limit=limit, before_id=before_id, skip_date_filter=True)
history = await manager.get_chat_history(
identity_key,
limit=limit,
before_id=before_id,
skip_date_filter=True,
date_from=date_from,
date_to=date_to,
)
next_cursor = history[-1]["id"] if history else None
logger.info(f"✅ Fetched {len(history)} messages for {identity_key}")
......
......@@ -127,6 +127,8 @@ class ConversationManager:
before_id: int | None = None,
include_product_ids: bool = True,
skip_date_filter: bool = False,
date_from: str | None = None,
date_to: str | None = None,
) -> list[dict[str, Any]]:
"""
Retrieve chat history for an identity (user_id or device_id) using cursor-based pagination.
......@@ -135,9 +137,29 @@ class ConversationManager:
skip_date_filter: True to get all history, False to get only today's messages
include_product_ids: True for API (frontend needs product cards),
False for AI context (only text needed)
date_from: Optional YYYY-MM-DD string (inclusive start)
date_to: Optional YYYY-MM-DD string (inclusive end)
"""
try:
if skip_date_filter:
if date_from or date_to:
# Explicit date range filter
base_query = sql.SQL("""
SELECT message, is_human, timestamp, id
FROM {table}
WHERE identity_key = %s
""").format(table=sql.Identifier(self.table_name))
params = [identity_key]
if date_from:
base_query = sql.SQL("{} AND timestamp >= %s").format(base_query)
params.append(date_from + " 00:00:00+07")
if date_to:
base_query = sql.SQL("{} AND timestamp < %s").format(base_query)
# date_to inclusive: add 1 day
from datetime import datetime as dt, timedelta
end = dt.strptime(date_to, "%Y-%m-%d") + timedelta(days=1)
params.append(end.strftime("%Y-%m-%d") + " 00:00:00+07")
elif skip_date_filter:
# Get all history without date filter
base_query = sql.SQL("""
SELECT message, is_human, timestamp, id
......
......@@ -8,328 +8,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/report_template.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/static/chart-templates.js"></script>
<style>
:root {
--bg-dark: #f0f2f5;
--bg-panel: #ffffff;
--border: #e2e8f0;
--text-main: #1e293b;
--text-muted: #64748b;
--accent: #7c3aed;
--accent-hover: #6d28d9;
--accent-light: #ede9fe;
--success: #059669;
--error: #dc2626;
--warn: #d97706;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg-dark);
color: var(--text-main);
height: 100vh;
display: flex;
overflow: hidden;
}
/* ── TWO PANE LAYOUT ── */
.layout-container { display: flex; width: 100%; height: 100%; }
/* ── LEFT PANE: CHAT ── */
.left-pane {
width: 420px;
background: var(--bg-panel);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: relative;
flex-shrink: 0;
box-shadow: 4px 0 15px rgba(0,0,0,0.02);
z-index: 10;
}
.chat-header {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: #faf5ff;
}
.chat-header h2 { font-size: 15px; font-weight: 700; color: var(--accent); }
.btn-icon {
background: none; border: none; cursor: pointer;
color: var(--text-muted); font-size: 16px;
padding: 8px; border-radius: 6px;
transition: all 0.2s;
}
.btn-icon:hover { background: var(--accent-light); color: var(--accent); }
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 16px;
scroll-behavior: smooth;
}
.chat-msg {
max-width: 92%;
padding: 12px 16px;
border-radius: 12px;
font-size: 13px;
line-height: 1.6;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.user-msg {
align-self: flex-end;
background: var(--accent);
color: white;
border-bottom-right-radius: 4px;
}
.ai-msg {
align-self: flex-start;
background: #f5f3ff;
color: var(--text-main);
border-bottom-left-radius: 4px;
width: 100%;
}
.agent-logs {
background: #1e1b4b;
color: #c4b5fd;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
max-height: 200px;
overflow-y: auto;
}
.agent-log-item { margin-bottom: 4px; padding-left: 10px; border-left: 2px solid #4c1d95; }
.agent-log-item.sql { color: #a78bfa; }
.agent-log-item.data { color: #34d399; }
.agent-log-item.think { color: #fbbf24; }
.chat-input-area {
padding: 16px;
border-top: 1px solid var(--border);
background: white;
}
.chat-input-wrapper {
position: relative;
display: flex;
align-items: flex-end;
background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 16px;
padding: 4px;
transition: border-color 0.2s;
}
.chat-input-wrapper:focus-within { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(124,58,237,0.1); }
.chat-input-wrapper textarea {
flex: 1; border: none; background: transparent;
padding: 12px; max-height: 120px; min-height: 44px;
resize: none; outline: none; font-family: inherit;
font-size: 13px; color: var(--text-main);
}
.btn-send {
width: 36px; height: 36px; border-radius: 12px;
border: none; background: var(--accent); color: white;
display: flex; align-items: center; justify-content: center;
cursor: pointer; margin: 4px; transition: all 0.2s;
}
.btn-send:hover { background: var(--accent-hover); transform: scale(1.05); }
.btn-send:disabled { background: #cbd5e1; cursor: not-allowed; transform: none; }
/* ── HISTORY SIDEBAR (Slide-over) ── */
.history-sidebar {
position: absolute;
top: 0; left: 0; bottom: 0;
width: 100%;
background: white;
z-index: 20;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex; flex-direction: column;
}
.history-sidebar.open { transform: translateX(0); }
.history-header {
padding: 16px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: center;
}
.conv-list { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.conv-item {
padding: 12px; border-radius: 10px; cursor: pointer;
border: 1px solid transparent; transition: all 0.2s;
}
.conv-item:hover { background: #f5f3ff; }
.conv-item.active { background: #ede9fe; border-color: #c4b5fd; }
.conv-title { font-size: 13px; font-weight: 600; line-height: 1.4; margin-bottom: 4px; }
.conv-meta { font-size: 11px; color: var(--text-muted); display: flex; gap: 8px; align-items: center; }
.pill {
display: inline-flex; align-items: center;
border-radius: 999px; padding: 2px 8px;
font-size: 10px; font-weight: 700;
letter-spacing: 0.04em; text-transform: uppercase;
}
.pill.completed { background: #ecfdf5; color: #059669; }
.pill.error { background: #fef2f2; color: #dc2626; }
.pill.running { background: #eff6ff; color: #2563eb; }
/* ── RIGHT PANE: TRACE DOCUMENT ── */
.right-pane {
flex: 1;
display: flex;
flex-direction: column;
background: #f5f3ff;
position: relative;
}
.doc-header {
height: 56px;
padding: 0 24px;
background: white;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
}
.doc-title { font-weight: 600; font-size: 14px; color: var(--text-main); }
.doc-actions { display: flex; gap: 8px; }
.btn-action {
padding: 7px 14px; border-radius: 8px; font-size: 12px; font-weight: 600;
cursor: pointer; border: 1px solid var(--border); background: white;
display: flex; align-items: center; gap: 6px; transition: all 0.2s;
}
.btn-action:hover { background: #f8fafc; }
.doc-scroll {
flex: 1; overflow-y: auto; padding: 24px;
display: flex; justify-content: center;
}
.doc-content {
width: 100%; max-width: 960px;
}
/* ── TRACE CARDS ── */
.trace-card {
background: white; border-radius: 12px; padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
border: 1px solid rgba(124,58,237,0.08);
animation: fadeIn 0.3s ease;
}
.trace-card-header {
display: flex; justify-content: space-between; align-items: flex-start;
margin-bottom: 12px;
}
.trace-card-label {
font-size: 11px; font-weight: 700; color: var(--accent);
text-transform: uppercase; letter-spacing: 0.05em;
}
.trace-card-title {
font-size: 16px; font-weight: 700; margin-top: 4px;
color: var(--text-main);
}
.trace-card-text {
font-size: 13px; line-height: 1.7; color: var(--text-muted);
margin-top: 8px;
}
/* Summary Grid */
.summary-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 10px; margin-top: 14px;
}
.summary-block {
padding: 14px; border-radius: 10px;
background: #faf5ff; border: 1px solid rgba(124,58,237,0.08);
}
.summary-block .label { font-size: 10px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: 0.06em; }
.summary-block .value { font-size: 22px; font-weight: 700; margin-top: 6px; color: var(--text-main); }
.summary-block .meta { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
/* SQL Block */
.sql-block {
background: #1e1b4b; color: #e9d5ff; border-radius: 10px;
padding: 14px; margin-top: 12px; overflow-x: auto;
font-family: 'JetBrains Mono', monospace; font-size: 12px;
line-height: 1.7; white-space: pre-wrap; word-break: break-word;
}
/* Query Item */
.query-item {
padding: 14px; border-radius: 10px; margin-top: 12px;
background: #faf5ff; border: 1px solid rgba(124,58,237,0.1);
}
.query-item-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 8px;
}
.query-item-name { font-size: 12px; font-weight: 700; color: var(--accent); text-transform: uppercase; }
.query-item-purpose { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; }
/* Preview Table */
.preview-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; margin-top: 10px; }
.preview-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.preview-table th, .preview-table td {
padding: 8px 10px; border-bottom: 1px solid #f1f5f9;
text-align: left; vertical-align: top;
}
.preview-table th { background: #f8fafc; color: var(--text-muted); font-weight: 700; font-size: 10px; text-transform: uppercase; }
.preview-table tbody tr:hover { background: #faf5ff; }
/* Reflect Box */
.reflect-box {
margin-top: 16px; padding: 14px; border-radius: 10px;
background: #fffbeb; border: 1px solid #fde68a;
}
.reflect-box .label { font-size: 11px; font-weight: 700; color: #b45309; text-transform: uppercase; }
.reflect-box p { font-size: 13px; color: #78350f; line-height: 1.6; margin-top: 6px; }
.reflect-box ul { font-size: 12px; color: #78350f; padding-left: 18px; margin-top: 6px; }
/* Empty State */
.empty-doc-state {
height: 100%; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #a78bfa; text-align: center;
}
.empty-icon { font-size: 64px; margin-bottom: 24px; opacity: 0.4; }
/* Loading dots */
.chat-typing { display: flex; gap: 4px; padding: 10px; align-items: center; }
.dot { width: 6px; height: 6px; background: #a78bfa; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out both; }
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
/* Chart cards */
.chart-card { background: white; border-radius: 12px; padding: 16px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); border: 1px solid rgba(124,58,237,0.08); }
.cc-header { margin-bottom: 12px; }
.cc-title { font-size: 14px; font-weight: 700; color: var(--text-main); }
.cc-desc { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.cc-body { position: relative; }
.cc-canvas-wrap { position: relative; height: 280px; }
.cc-footer { font-size: 11px; color: var(--text-muted); margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.cc-meta { display: flex; gap: 12px; font-size: 10px; color: var(--text-muted); margin-top: 8px; }
.cc-trend { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
.cc-trend.up { color: #059669; } .cc-trend.down { color: #dc2626; }
.data-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; }
.data-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.data-table th, .data-table td { padding: 8px 10px; border-bottom: 1px solid #f1f5f9; text-align: left; }
.data-table th { background: #f8fafc; color: var(--text-muted); font-weight: 700; font-size: 10px; text-transform: uppercase; }
.data-table tbody tr:hover { background: #faf5ff; }
</style>
<script src="/static/js/chart-templates.js"></script>
<link rel="stylesheet" href="/static/css/ai-sql.css">
</head>
<body>
......@@ -343,10 +23,10 @@ body {
<h2 id="chatTitle">AI SQL Trace</h2>
</div>
<div style="display:flex; align-items:center; gap: 8px;">
<div style="display:flex; align-items:center; background: white; border: 1px solid var(--border); padding: 5px 10px; border-radius: 8px; gap: 6px;" title="Chọn Mô hình AI">
<i class="fas fa-brain" style="color: var(--accent); font-size: 13px;"></i>
<select id="modelSelect" style="border: none; background: transparent; color: var(--text-main); font-size: 12px; font-weight: 600; outline: none; cursor: pointer; appearance: none; -webkit-appearance: none; padding-right: 0px;">
<div style="display:flex; align-items:center; gap: 6px;">
<div style="display:flex; align-items:center; background: var(--card); border: 1px solid var(--border); padding: 5px 10px; border-radius: 8px; gap: 6px;" title="Chọn Mô hình AI">
<i class="fas fa-brain" style="color: var(--muted-fg); font-size: 13px;"></i>
<select id="modelSelect" style="border: none; background: transparent; color: var(--foreground); font-size: 12px; font-weight: 600; outline: none; cursor: pointer; appearance: none; -webkit-appearance: none; padding-right: 0px;">
<optgroup label="Codex">
<option value="codex/gpt-5.3-codex">🤖 GPT-5.3 Codex</option>
</optgroup>
......@@ -356,8 +36,9 @@ body {
<option value="openai/gpt-5.4-mini">⚡ GPT-5.4 Mini</option>
</optgroup>
</select>
<i class="fas fa-caret-down" style="color: var(--text-muted); font-size: 10px;"></i>
<i class="fas fa-caret-down" style="color: var(--muted-fg); font-size: 10px;"></i>
</div>
<button class="btn-icon" onclick="openSettings()" title="Settings"><i class="fas fa-cog"></i></button>
<button class="btn-icon" onclick="startNewConversation()" title="Tạo mới"><i class="fas fa-edit"></i></button>
</div>
</div>
......@@ -414,7 +95,309 @@ body {
</div>
<!-- SETTINGS MODAL (Memos-style: sidebar + content) -->
<div id="settingsModal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.25); z-index:100; backdrop-filter:blur(2px);">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:680px; max-width:92vw; height:520px; max-height:85vh; background:var(--card); border:1px solid var(--border); border-radius:12px; box-shadow:var(--shadow-lg); display:flex; flex-direction:column; overflow:hidden;">
<!-- Two-column body -->
<div style="display:flex; flex:1; min-height:0;">
<!-- LEFT SIDEBAR -->
<div style="width:180px; border-right:1px solid var(--border); display:flex; flex-direction:column; padding:16px 12px; gap:2px; background:var(--card);">
<span style="font-size:11px; font-weight:600; color:var(--muted-fg); padding:4px 12px; text-transform:uppercase; letter-spacing:0.05em;">Basic</span>
<div class="section-menu-item active" onclick="switchSettingTab('connection')" data-tab="connection">
<i class="fas fa-database icon"></i> <span>Kết nối</span>
</div>
<div class="section-menu-item" onclick="switchSettingTab('model')" data-tab="model">
<i class="fas fa-brain icon"></i> <span>Mô hình AI</span>
</div>
<div class="section-menu-item" onclick="switchSettingTab('display')" data-tab="display">
<i class="fas fa-palette icon"></i> <span>Hiển thị</span>
</div>
<span style="font-size:11px; font-weight:600; color:var(--muted-fg); padding:4px 12px; margin-top:16px; text-transform:uppercase; letter-spacing:0.05em;">Admin</span>
<div class="section-menu-item" onclick="switchSettingTab('advanced')" data-tab="advanced">
<i class="fas fa-cog icon"></i> <span>Nâng cao</span>
</div>
<!-- Spacer -->
<div style="flex:1;"></div>
<!-- User row (Memos-style) -->
<div style="border-top:1px solid var(--border); padding-top:12px; margin-top:8px;">
<div style="display:flex; align-items:center; gap:8px; padding:4px 8px;">
<div style="width:28px; height:28px; border-radius:50%; background:var(--muted); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:600; color:var(--muted-fg);">U</div>
<span style="flex:1; font-size:13px; font-weight:500; color:var(--foreground);">user</span>
<button class="btn-icon" style="padding:4px;" title="Settings"><i class="fas fa-cog" style="font-size:12px;"></i></button>
<button class="btn-icon" style="padding:4px;" title="Copy ID"><i class="fas fa-copy" style="font-size:12px;"></i></button>
</div>
<div style="display:flex; align-items:center; gap:6px; padding:6px 8px 0; font-size:11px; color:var(--muted-fg);">
<span style="width:6px; height:6px; border-radius:50%; background:var(--success);"></span>
v2.5.0 · Online
</div>
</div>
</div>
<!-- RIGHT CONTENT PANEL -->
<div style="flex:1; display:flex; flex-direction:column; min-width:0;">
<!-- Content scrollable area -->
<div style="flex:1; overflow-y:auto; padding:20px 24px;">
<!-- TAB: Connection -->
<div id="settingTab_connection" class="setting-tab-content">
<div class="setting-section">
<div class="setting-section-header">
<h3>Kết nối Database</h3>
<p class="desc">Cấu hình endpoint và database connection</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">API Endpoint</span>
<p class="setting-label-desc">URL backend server</p>
</div>
<div class="setting-control">
<input class="input" id="settingApiUrl" style="width:220px;" value="" placeholder="http://localhost:5000">
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">StarRocks Host</span>
<p class="setting-label-desc">Địa chỉ database StarRocks</p>
</div>
<div class="setting-control">
<input class="input" id="settingDbHost" style="width:220px;" value="" placeholder="172.16.2.xxx:9030">
</div>
</div>
<hr class="separator">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Trạng thái</span>
<p class="setting-label-desc">Kiểm tra kết nối hiện tại</p>
</div>
<div class="setting-control">
<button class="btn btn-outline btn-sm" onclick="testConnection()"><i class="fas fa-plug" style="margin-right:4px;"></i> Test</button>
</div>
</div>
</div>
</div>
</div>
<!-- TAB: Model -->
<div id="settingTab_model" class="setting-tab-content" style="display:none;">
<div class="setting-section">
<div class="setting-section-header">
<h3>Mô hình AI</h3>
<p class="desc">Chọn model và cấu hình agent</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Default Model</span>
<p class="setting-label-desc">Mô hình mặc định khi tạo mới</p>
</div>
<div class="setting-control">
<select class="select" id="settingDefaultModel">
<option value="codex/gpt-5.3-codex">GPT-5.3 Codex</option>
<option value="openai/gpt-4o">GPT-4o</option>
<option value="openai/gpt-5.4-nano">GPT-5.4 Nano</option>
<option value="openai/gpt-5.4-mini">GPT-5.4 Mini</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Max Cycles</span>
<p class="setting-label-desc">Số vòng lặp tối đa cho agent</p>
</div>
<div class="setting-control">
<input class="input" id="settingMaxCycles" type="number" style="width:80px;" value="3" min="1" max="10">
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Temperature</span>
<p class="setting-label-desc">Độ sáng tạo của model (0-1)</p>
</div>
<div class="setting-control">
<input class="input" id="settingTemperature" type="number" style="width:80px;" value="0" min="0" max="1" step="0.1">
</div>
</div>
</div>
</div>
</div>
<!-- TAB: Display -->
<div id="settingTab_display" class="setting-tab-content" style="display:none;">
<div class="setting-section">
<div class="setting-section-header">
<h3>Hiển thị</h3>
<p class="desc">Tùy chỉnh giao diện và rendering</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Tự động vẽ Chart</span>
<p class="setting-label-desc">Tự động render biểu đồ từ kết quả SQL</p>
</div>
<div class="setting-control">
<button class="switch on" id="settingAutoChart" onclick="this.classList.toggle('on')"></button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Hiển thị SQL</span>
<p class="setting-label-desc">Luôn hiện SQL block trong kết quả</p>
</div>
<div class="setting-control">
<button class="switch on" id="settingShowSql" onclick="this.classList.toggle('on')"></button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Hiển thị Agent Logs</span>
<p class="setting-label-desc">Hiện log agent trong chat</p>
</div>
<div class="setting-control">
<button class="switch on" id="settingShowLogs" onclick="this.classList.toggle('on')"></button>
</div>
</div>
</div>
</div>
</div>
<!-- TAB: Advanced -->
<div id="settingTab_advanced" class="setting-tab-content" style="display:none;">
<div class="setting-section">
<div class="setting-section-header">
<h3>Nâng cao</h3>
<p class="desc">Cấu hình nâng cao cho admin</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Human-in-the-Loop</span>
<p class="setting-label-desc">Yêu cầu phê duyệt trước khi chạy SQL</p>
</div>
<div class="setting-control">
<button class="switch on" id="settingHitl" onclick="this.classList.toggle('on')"></button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Debug Mode</span>
<p class="setting-label-desc">Hiện thông tin debug chi tiết</p>
</div>
<div class="setting-control">
<button class="switch" id="settingDebug" onclick="this.classList.toggle('on')"></button>
</div>
</div>
<hr class="separator">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Reset Settings</span>
<p class="setting-label-desc">Xóa tất cả cài đặt về mặc định</p>
</div>
<div class="setting-control">
<button class="btn btn-outline btn-sm" style="color:var(--error);" onclick="resetSettings()"><i class="fas fa-trash-alt" style="margin-right:4px;"></i> Reset</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div style="padding:12px 24px; border-top:1px solid var(--border); display:flex; justify-content:flex-end; gap:8px; background:var(--card);">
<button class="btn btn-outline" onclick="closeSettings()">Hủy</button>
<button class="btn btn-primary" onclick="saveSettings()"><i class="fas fa-check" style="margin-right:4px;"></i> Lưu</button>
</div>
</div>
</div>
</div>
</div>
<script>
// ─── SETTINGS ───
function switchSettingTab(tabName) {
// Hide all tabs
document.querySelectorAll('.setting-tab-content').forEach(el => el.style.display = 'none');
// Show selected tab
const tab = document.getElementById('settingTab_' + tabName);
if (tab) tab.style.display = '';
// Update sidebar active state
document.querySelectorAll('#settingsModal .section-menu-item').forEach(el => {
el.classList.toggle('active', el.dataset.tab === tabName);
});
}
function openSettings() {
const modal = document.getElementById('settingsModal');
const saved = JSON.parse(localStorage.getItem('aiSqlSettings') || '{}');
document.getElementById('settingApiUrl').value = saved.apiUrl || window.location.origin;
document.getElementById('settingDbHost').value = saved.dbHost || '';
document.getElementById('settingDefaultModel').value = saved.defaultModel || 'codex/gpt-5.3-codex';
document.getElementById('settingMaxCycles').value = saved.maxCycles || 3;
document.getElementById('settingTemperature').value = saved.temperature || 0;
// Switches
['settingAutoChart', 'settingShowSql', 'settingShowLogs', 'settingHitl'].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
const key = id.replace('setting', '').replace(/^./, c => c.toLowerCase());
if (saved[key] === false) el.classList.remove('on'); else el.classList.add('on');
});
if (saved.debug) document.getElementById('settingDebug').classList.add('on');
else document.getElementById('settingDebug').classList.remove('on');
// Reset to first tab
switchSettingTab('connection');
modal.style.display = 'block';
}
function closeSettings() {
document.getElementById('settingsModal').style.display = 'none';
}
function saveSettings() {
const settings = {
apiUrl: document.getElementById('settingApiUrl').value,
dbHost: document.getElementById('settingDbHost').value,
defaultModel: document.getElementById('settingDefaultModel').value,
maxCycles: parseInt(document.getElementById('settingMaxCycles').value) || 3,
temperature: parseFloat(document.getElementById('settingTemperature').value) || 0,
autoChart: document.getElementById('settingAutoChart').classList.contains('on'),
showSql: document.getElementById('settingShowSql').classList.contains('on'),
showLogs: document.getElementById('settingShowLogs').classList.contains('on'),
hitl: document.getElementById('settingHitl').classList.contains('on'),
debug: document.getElementById('settingDebug').classList.contains('on'),
};
localStorage.setItem('aiSqlSettings', JSON.stringify(settings));
document.getElementById('modelSelect').value = settings.defaultModel;
closeSettings();
}
function testConnection() {
const btn = event.target.closest('button');
const origHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right:4px;"></i> Testing...';
btn.disabled = true;
fetch('/health').then(r => r.json()).then(data => {
btn.innerHTML = '<i class="fas fa-check" style="margin-right:4px; color:var(--success);"></i> OK';
setTimeout(() => { btn.innerHTML = origHTML; btn.disabled = false; }, 2000);
}).catch(err => {
btn.innerHTML = '<i class="fas fa-times" style="margin-right:4px; color:var(--error);"></i> Fail';
setTimeout(() => { btn.innerHTML = origHTML; btn.disabled = false; }, 2000);
});
}
function resetSettings() {
if (confirm('Xóa tất cả cài đặt về mặc định?')) {
localStorage.removeItem('aiSqlSettings');
closeSettings();
openSettings(); // Re-open with defaults
}
}
// Close on backdrop click
document.getElementById('settingsModal').addEventListener('click', function(e) {
if (e.target === this) closeSettings();
});
// ─── STATE ───
let currentConversationId = null;
let currentSession = null;
......@@ -845,7 +828,7 @@ body {
// AI Summary card (if available)
const summaryHtml = session.ai_summary ? `
<div class="trace-card" style="border-left:4px solid #7c3aed; background:linear-gradient(135deg, #faf5ff 0%, #ffffff 100%);">
<div class="trace-card" style="border-left:4px solid var(--primary);">
<div style="display:flex; align-items:flex-start; gap:10px;">
<span style="font-size:18px;">🤖</span>
<div style="font-size:13px; color:#334155; line-height:1.6;">${escapeHTML(session.ai_summary)}</div>
......
......@@ -4,9 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cache Manager — Canifa AI</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/product.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/product.css">
<script src="/static/js/frame-detect.js"></script>
<style>
/* ═══ CACHE PAGE ═══ */
.cache-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-bottom: 24px; }
......
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog - Canifa AI System</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: var(--bg, #FAF6F0); color: var(--t, #2C1810); min-height: 100vh; display: flex; }
/* ═══ SIDEBAR ═══ */
/* --- SIDEBAR --- */
.sidebar { width: 260px; min-height: 100vh; background: var(--s, #FFFFFF); border-right: 1px solid var(--b, #E8DED0); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; z-index: 100; }
.sidebar-brand { padding: 24px 20px; border-bottom: 1px solid var(--b, #E8DED0); display: flex; align-items: center; gap: 12px; }
.brand-icon { width: 40px; height: 40px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.3em; flex-shrink: 0; }
......@@ -35,21 +37,21 @@
.version-dot { width: 8px; height: 8px; border-radius: 50%; background: #56d364; box-shadow: 0 0 8px rgba(86,211,100,0.4); }
.version-text { font-size: 0.75em; color: var(--m, #6B5B4F); }
/* ═══ MAIN ═══ */
/* --- MAIN --- */
.main { margin-left: 260px; flex: 1; min-height: 100vh; display: flex; flex-direction: column; }
.topbar { padding: 24px 32px; border-bottom: 1px solid var(--b, #E8DED0); display: flex; justify-content: space-between; align-items: center; }
.topbar h1 { font-size: 1.5em; font-weight: 800; color: var(--t, #2C1810); }
.topbar p { font-size: 0.85em; color: #484f58; margin-top: 2px; }
.content { padding: 24px 32px; flex: 1; max-width: 800px; }
/* ═══ FORM ═══ */
/* --- FORM --- */
.form-input { width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--b, #E8DED0); background: var(--s, #FFFFFF); color: var(--t, #2C1810); font-size: 0.88em; font-family: inherit; transition: border-color 0.2s; }
.form-input:focus { outline: none; border-color: #667eea; }
.btn { padding: 8px 16px; border-radius: 8px; font-size: 0.82em; font-weight: 600; border: 1px solid var(--b, #E8DED0); background: var(--bg, #FAF6F0); color: var(--t, #2C1810); cursor: pointer; transition: all 0.2s; }
.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2); border: none; color: #fff; }
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
/* ═══ CHANGELOG ═══ */
/* --- CHANGELOG --- */
.cl-form { display: flex; gap: 10px; margin-bottom: 24px; background: var(--s, #FFFFFF); border: 1px solid var(--b, #E8DED0); border-radius: 12px; padding: 14px; }
.cl-form input { flex: 1; }
.cl-form .author { max-width: 140px; }
......@@ -70,7 +72,7 @@
.empty-state { text-align: center; padding: 60px 20px; color: #484f58; }
.empty-state .empty-icon { font-size: 3em; margin-bottom: 12px; opacity: 0.5; }
/* ═══ USAGE GUIDE ═══ */
/* --- USAGE GUIDE --- */
.guide { margin-top: 40px; border-top: 1px solid var(--b, #E8DED0); padding-top: 20px; }
.guide-toggle { display: flex; align-items: center; gap: 8px; cursor: pointer; color: #484f58; font-size: 0.82em; font-weight: 600; background: none; border: none; padding: 6px 0; transition: color 0.2s; }
.guide-toggle:hover { color: var(--m, #6B5B4F); }
......@@ -96,51 +98,51 @@
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">🤖</div>
<div class="brand-icon">??</div>
<div class="brand-text"><h2>Canifa AI</h2><span>Admin Console</span></div>
</div>
<div class="nav-group">
<div class="nav-group-label">Main</div>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">🔀</span><span>Sơ đồ hoạt động</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">💬</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">🧾</span><span>History</span></a>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">??</span><span>So d? ho?t d?ng</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">??</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">??</span><span>History</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Workspace</div>
<a href="/static/resources.html" class="nav-item"><span class="nav-icon">🔗</span><span>Resources</span></a>
<a href="/static/notes.html" class="nav-item"><span class="nav-icon">📝</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item active"><span class="nav-icon">📋</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item"><span class="nav-icon">📖</span><span>Hướng dẫn</span></a>
<a href="/static/resources.html" class="nav-item"><span class="nav-icon">??</span><span>Resources</span></a>
<a href="/static/notes.html" class="nav-item"><span class="nav-icon">??</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item active"><span class="nav-icon">??</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item"><span class="nav-icon">??</span><span>Hu?ng d?n</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Thử nghiệm</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">🗄️</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">🔍</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">📝</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
<div class="nav-group-label">Th? nghi?m</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">???</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">??</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">??</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">External</div>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">📚</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">📖</span><span>ReDoc</span></a>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>ReDoc</span></a>
</div>
<div class="sidebar-footer">
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> · Online</span></div>
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> Online</span></div>
</div>
</aside>
<div class="main">
<div class="topbar">
<div><h1>📋 Changelog</h1><p>Lịch sử thay đổi chatbot</p></div>
<div><h1>?? Changelog</h1><p>L?ch s? thay d?i chatbot</p></div>
</div>
<div class="content">
<div class="cl-form">
<input type="text" class="form-input author" id="clAuthor" placeholder="Tên bạn" value="Admin">
<input type="text" class="form-input" id="clContent" placeholder="Bạn đã thay đổi gì? VD: Sửa prompt thêm rule không bịa sản phẩm...">
<button class="btn btn-primary" onclick="add()">+ Thêm</button>
<input type="text" class="form-input author" id="clAuthor" placeholder="Tn b?n" value="Admin">
<input type="text" class="form-input" id="clContent" placeholder="B?n d thay d?i g? VD: S?a prompt thm rule khng b?a s?n ph?m...">
<button class="btn btn-primary" onclick="add()">+ Thm</button>
</div>
<div id="list">
<div class="empty-state"><div class="empty-icon">📋</div><p>Chưa có ghi chú nào. Thêm dòng đầu tiên!</p></div>
<div class="empty-state"><div class="empty-icon">??</div><p>Chua c ghi ch no. Thm dng d?u tin!</p></div>
</div>
......@@ -150,7 +152,7 @@
<script>
let data = [];
function esc(s) { return s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : ''; }
function fmtTime(iso) { if(!iso)return''; const d=new Date(iso),now=new Date(),diff=(now-d)/1000; if(diff<60)return'vừa xong'; if(diff<3600)return Math.floor(diff/60)+' phút trước'; if(diff<86400)return Math.floor(diff/3600)+' giờ trước'; if(diff<604800)return Math.floor(diff/86400)+' ngày trước'; return d.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit',year:'numeric'}); }
function fmtTime(iso) { if(!iso)return''; const d=new Date(iso),now=new Date(),diff=(now-d)/1000; if(diff<60)return'v?a xong'; if(diff<3600)return Math.floor(diff/60)+' pht tru?c'; if(diff<86400)return Math.floor(diff/3600)+' gi? tru?c'; if(diff<604800)return Math.floor(diff/86400)+' ngy tru?c'; return d.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit',year:'numeric'}); }
async function load() {
try { const r=await fetch('/api/dashboard/changelog'); const j=await r.json(); data=j.entries||[]; render(); }
......@@ -159,7 +161,7 @@
function render() {
const el = document.getElementById('list');
if (!data.length) { el.innerHTML = `<div class="empty-state"><div class="empty-icon">📋</div><p>Chưa có ghi chú nào. Thêm dòng đầu tiên!</p></div>`; return; }
if (!data.length) { el.innerHTML = `<div class="empty-state"><div class="empty-icon">??</div><p>Chua c ghi ch no. Thm dng d?u tin!</p></div>`; return; }
el.innerHTML = data.map((e,i) => `
<div class="cl-entry">
<div class="cl-dot"></div>
......@@ -171,7 +173,7 @@
</div>
<div class="cl-content">${esc(e.content)}</div>
</div>
<button class="cl-delete" title="Xóa" onclick="del('${e.id}')">🗑️</button>
<button class="cl-delete" title="Xa" onclick="del('${e.id}')">???</button>
</div>`).join('');
}
......@@ -183,7 +185,7 @@
}
async function del(id) {
if(!confirm('Xóa ghi chú này?'))return;
if(!confirm('Xa ghi ch ny?'))return;
try { await fetch(`/api/dashboard/changelog/${id}`,{method:'DELETE'}); await load(); }
catch(e) { alert(e.message); }
}
......
/* ═══════════════════════════════════════════════════════════
AI SQL Trace — Page-Specific Styles
Theme vars come from theme.css, components from components.css
═══════════════════════════════════════════════════════════ */
@import url('theme.css');
@import url('components.css');
/* ── Page-specific body override ── */
body {
font-family: var(--font-sans);
background: var(--background);
color: var(--foreground);
height: 100vh;
display: flex;
overflow: hidden;
margin: 0; padding: 0;
}
/* ══════════════════════════════════
LAYOUT: Two Pane
══════════════════════════════════ */
.layout-container { display: flex; width: 100%; height: 100%; }
/* ── LEFT PANE: Chat ── */
.left-pane {
width: 420px;
background: var(--card);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: relative;
flex-shrink: 0;
z-index: 10;
}
.chat-header {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--card);
}
.chat-header h2 {
font-size: 14px;
font-weight: 600;
color: var(--foreground);
letter-spacing: -0.01em;
}
.btn-icon {
background: none;
border: none;
cursor: pointer;
color: var(--muted-fg);
font-size: 15px;
padding: 8px;
border-radius: var(--radius);
transition: all 0.15s;
}
.btn-icon:hover {
background: var(--muted);
color: var(--foreground);
}
/* ── Chat Messages ── */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 14px;
scroll-behavior: smooth;
}
.chat-msg {
max-width: 92%;
padding: 10px 14px;
border-radius: 14px;
font-size: 13px;
line-height: 1.6;
animation: fadeIn 0.25s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.user-msg {
align-self: flex-end;
background: var(--primary);
color: white;
border-bottom-right-radius: 4px;
}
.ai-msg {
align-self: flex-start;
background: var(--muted);
color: var(--foreground);
border-bottom-left-radius: 4px;
width: 100%;
}
/* ── Agent Logs (dark terminal) ── */
.agent-logs {
background: #1a1a2e;
color: #a5b4cb;
font-family: var(--font-mono);
font-size: 11px;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
max-height: 200px;
overflow-y: auto;
}
.agent-log-item {
margin-bottom: 4px;
padding-left: 10px;
border-left: 2px solid #334155;
}
.agent-log-item.sql { color: #818cf8; }
.agent-log-item.data { color: #34d399; }
.agent-log-item.think { color: #fbbf24; }
/* ── Chat Input ── */
.chat-input-area {
padding: 14px 16px;
border-top: 1px solid var(--border);
background: var(--card);
}
.chat-input-wrapper {
position: relative;
display: flex;
align-items: flex-end;
background: var(--background);
border: 1px solid var(--input);
border-radius: 14px;
padding: 4px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.chat-input-wrapper:focus-within {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 89, 152, 0.1);
}
.chat-input-wrapper textarea {
flex: 1;
border: none;
background: transparent;
padding: 10px 12px;
max-height: 120px;
min-height: 42px;
resize: none;
outline: none;
font-family: inherit;
font-size: 13px;
color: var(--foreground);
}
.btn-send {
width: 34px;
height: 34px;
border-radius: 10px;
border: none;
background: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 3px;
transition: all 0.15s;
}
.btn-send:hover { background: var(--primary-hover); transform: scale(1.04); }
.btn-send:disabled { background: var(--input); cursor: not-allowed; transform: none; }
/* ══════════════════════════════════
HISTORY SIDEBAR (Slide-over)
══════════════════════════════════ */
.history-sidebar {
position: absolute;
top: 0; left: 0; bottom: 0;
width: 100%;
background: var(--card);
z-index: 20;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
}
.history-sidebar.open { transform: translateX(0); }
.history-header {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.conv-list {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.conv-item {
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.15s;
}
.conv-item:hover { background: var(--muted); }
.conv-item.active { background: var(--primary-light); border-color: var(--primary); }
.conv-title { font-size: 13px; font-weight: 600; line-height: 1.4; margin-bottom: 3px; }
.conv-meta { font-size: 11px; color: var(--muted-fg); display: flex; gap: 8px; align-items: center; }
/* ── Pill badges ── */
.pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 2px 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.pill.completed { background: #ecfdf5; color: var(--success); }
.pill.error { background: #fef2f2; color: var(--error); }
.pill.running { background: #eff6ff; color: #2563eb; }
/* ══════════════════════════════════
RIGHT PANE: Data / Chart
══════════════════════════════════ */
.right-pane {
flex: 1;
display: flex;
flex-direction: column;
background: var(--background);
position: relative;
}
.doc-header {
height: 52px;
padding: 0 20px;
background: var(--card);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.doc-title { font-weight: 600; font-size: 13px; color: var(--foreground); }
.doc-actions { display: flex; gap: 6px; }
.btn-action {
padding: 6px 12px;
border-radius: var(--radius);
font-size: 12px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
background: var(--card);
display: flex;
align-items: center;
gap: 5px;
transition: all 0.15s;
color: var(--foreground);
}
.btn-action:hover { background: var(--muted); }
.doc-scroll {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
justify-content: center;
}
.doc-content {
width: 100%;
max-width: 920px;
}
/* ══════════════════════════════════
TRACE CARDS (Memos card style)
══════════════════════════════════ */
.trace-card {
background: var(--card);
border-radius: 10px;
padding: 18px;
margin-bottom: 14px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
animation: fadeIn 0.25s ease;
}
.trace-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}
.trace-card-label {
font-size: 11px;
font-weight: 600;
color: var(--primary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.trace-card-title {
font-size: 15px;
font-weight: 700;
margin-top: 3px;
color: var(--foreground);
}
.trace-card-text {
font-size: 13px;
line-height: 1.7;
color: var(--muted-fg);
margin-top: 6px;
}
/* ── Summary Grid (Number Cards) ── */
.summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 12px;
}
.summary-block {
padding: 12px;
border-radius: 8px;
background: var(--muted);
border: 1px solid var(--border);
}
.summary-block .label {
font-size: 10px;
font-weight: 600;
color: var(--muted-fg);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.summary-block .value {
font-size: 22px;
font-weight: 700;
margin-top: 4px;
color: var(--foreground);
}
.summary-block .meta {
font-size: 11px;
color: var(--muted-fg);
margin-top: 3px;
}
/* ── SQL Block ── */
.sql-block {
background: #1a1a2e;
color: #d1d5db;
border-radius: 8px;
padding: 14px;
margin-top: 10px;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
}
/* ── Query Item ── */
.query-item {
padding: 12px;
border-radius: 8px;
margin-top: 10px;
background: var(--muted);
border: 1px solid var(--border);
}
.query-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.query-item-name {
font-size: 12px;
font-weight: 600;
color: var(--primary);
text-transform: uppercase;
}
.query-item-purpose {
font-size: 12px;
color: var(--muted-fg);
margin-bottom: 8px;
}
/* ── Data Tables — clean minimal ── */
.preview-table-wrap,
.data-table-wrap {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: 8px;
margin-top: 10px;
}
.preview-table,
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
}
.preview-table th,
.preview-table td,
.data-table th,
.data-table td {
padding: 8px 10px;
border-bottom: 1px solid var(--border);
text-align: left;
vertical-align: top;
}
.preview-table th,
.data-table th {
background: var(--muted);
color: var(--muted-fg);
font-weight: 600;
font-size: 10px;
text-transform: uppercase;
}
.preview-table tbody tr:hover,
.data-table tbody tr:hover {
background: var(--primary-light);
}
/* ── Reflect Box ── */
.reflect-box {
margin-top: 14px;
padding: 12px;
border-radius: 8px;
background: #fefce8;
border: 1px solid #fde68a;
}
.reflect-box .label { font-size: 11px; font-weight: 600; color: #a16207; text-transform: uppercase; }
.reflect-box p { font-size: 13px; color: #713f12; line-height: 1.6; margin-top: 4px; }
.reflect-box ul { font-size: 12px; color: #713f12; padding-left: 18px; margin-top: 4px; }
/* ── Empty State ── */
.empty-doc-state {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--muted-fg);
text-align: center;
}
.empty-icon { font-size: 56px; margin-bottom: 20px; opacity: 0.25; }
/* ── Loading Dots ── */
.chat-typing { display: flex; gap: 4px; padding: 10px; align-items: center; }
.dot {
width: 5px;
height: 5px;
background: var(--primary);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
opacity: 0.6;
}
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
/* ══════════════════════════════════
CHART CARDS
══════════════════════════════════ */
.chart-card {
background: var(--card);
border-radius: 10px;
padding: 16px;
margin-bottom: 14px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
}
.cc-header { margin-bottom: 10px; }
.cc-title { font-size: 14px; font-weight: 600; color: var(--foreground); }
.cc-desc { font-size: 12px; color: var(--muted-fg); margin-top: 3px; }
.cc-body { position: relative; }
.cc-canvas-wrap { position: relative; height: 280px; }
.cc-footer {
font-size: 11px;
color: var(--muted-fg);
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.cc-meta { display: flex; gap: 12px; font-size: 10px; color: var(--muted-fg); margin-top: 8px; }
.cc-trend { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
.cc-trend.up { color: var(--success); }
.cc-trend.down { color: var(--error); }
/* ═══════════════════════════════════════════════════════════
Canifa AI Platform — Shared UI Components
Inspired by Memos SettingRow, SettingSection, SearchBar
═══════════════════════════════════════════════════════════ */
/* ── Setting Section ─────────────────────────────────── */
.setting-section {
display: flex;
flex-direction: column;
gap: 16px;
padding: 8px 0 16px;
}
.setting-section-header {
display: flex;
flex-direction: column;
gap: 4px;
}
.setting-section-header h3 {
font-size: 15px;
font-weight: 600;
color: var(--foreground);
margin: 0;
}
.setting-section-header .desc {
font-size: 13px;
color: var(--muted-fg);
margin: 0;
}
.setting-section-body {
display: flex;
flex-direction: column;
gap: 16px;
}
/* ── Setting Group ───────────────────────────────────── */
.setting-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.setting-group-title {
font-size: 13px;
font-weight: 500;
color: var(--muted-fg);
margin: 0;
}
.setting-separator {
border: none;
border-top: 1px solid var(--border);
margin: 8px 0;
}
/* ── Setting Row (label ←→ control) ──────────────────── */
.setting-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 12px;
width: 100%;
}
.setting-row.vertical {
flex-direction: column;
align-items: stretch;
}
.setting-label {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.setting-label-text {
font-size: 13px;
color: var(--foreground);
}
.setting-row.vertical .setting-label-text {
font-weight: 500;
}
.setting-label-desc {
font-size: 11px;
color: var(--muted-fg);
margin: 0;
}
.setting-control {
display: flex;
align-items: center;
flex-shrink: 0;
}
/* ── Section Menu Item (sidebar nav) ─────────────────── */
.section-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
color: var(--muted-fg);
transition: all 0.15s;
user-select: none;
width: 100%;
}
.section-menu-item:hover {
background: var(--muted);
color: var(--foreground);
}
.section-menu-item.active {
background: var(--accent);
color: var(--foreground);
box-shadow: var(--shadow-xs);
}
.section-menu-item i,
.section-menu-item .icon {
font-size: 14px;
width: 16px;
text-align: center;
opacity: 0.8;
flex-shrink: 0;
}
/* ── Search Bar (icon-prefix input) ──────────────────── */
.search-bar {
position: relative;
width: 100%;
}
.search-bar .search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 13px;
color: var(--muted-fg);
opacity: 0.5;
pointer-events: none;
}
.search-bar input {
width: 100%;
padding: 7px 12px 7px 32px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--card);
color: var(--foreground);
font-size: 13px;
font-family: var(--font-sans);
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.search-bar input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 89, 152, 0.1);
}
.search-bar input::placeholder {
color: var(--muted-fg);
opacity: 0.6;
}
/* ── Card (base Memos-style card) ────────────────────── */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 16px;
box-shadow: var(--shadow-sm);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: var(--foreground);
margin: 0;
}
.card-desc {
font-size: 12px;
color: var(--muted-fg);
margin: 2px 0 0;
}
.card-body {
display: flex;
flex-direction: column;
gap: 12px;
}
/* ── Badge / Pill ────────────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 2px 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.badge-success { background: var(--success-light); color: var(--success); }
.badge-error { background: var(--error-light); color: var(--error); }
.badge-warn { background: var(--warn-light); color: var(--warn); }
.badge-info { background: var(--info-light); color: var(--info); }
.badge-muted { background: var(--muted); color: var(--muted-fg); }
/* ── Buttons ─────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 7px 14px;
border-radius: var(--radius);
font-size: 13px;
font-weight: 500;
font-family: var(--font-sans);
cursor: pointer;
border: 1px solid transparent;
transition: all 0.15s;
user-select: none;
}
.btn-primary {
background: var(--primary);
color: var(--primary-fg);
border-color: var(--primary);
}
.btn-primary:hover { background: var(--primary-hover); }
.btn-outline {
background: var(--card);
color: var(--foreground);
border-color: var(--border);
}
.btn-outline:hover { background: var(--muted); }
.btn-ghost {
background: transparent;
color: var(--muted-fg);
border-color: transparent;
}
.btn-ghost:hover { background: var(--muted); color: var(--foreground); }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-icon-only {
width: 32px; height: 32px;
padding: 0;
border-radius: var(--radius);
}
/* ── Select / Dropdown ───────────────────────────────── */
.select {
padding: 6px 28px 6px 10px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--card);
color: var(--foreground);
font-size: 13px;
font-family: var(--font-sans);
outline: none;
appearance: none;
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2378716c' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
transition: border-color 0.15s;
}
.select:focus { border-color: var(--primary); }
.select:hover { border-color: var(--input); }
/* ── Input Text ──────────────────────────────────────── */
.input {
padding: 7px 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--card);
color: var(--foreground);
font-size: 13px;
font-family: var(--font-sans);
outline: none;
width: 100%;
transition: border-color 0.15s, box-shadow 0.15s;
}
.input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 89, 152, 0.1);
}
/* ── Switch Toggle ───────────────────────────────────── */
.switch {
position: relative;
width: 36px;
height: 20px;
border-radius: 999px;
background: var(--input);
cursor: pointer;
transition: background 0.2s;
border: none;
padding: 0;
}
.switch::after {
content: '';
position: absolute;
top: 2px; left: 2px;
width: 16px; height: 16px;
border-radius: 50%;
background: white;
box-shadow: var(--shadow-xs);
transition: transform 0.2s;
}
.switch.on { background: var(--primary); }
.switch.on::after { transform: translateX(16px); }
/* ── Table (clean minimal) ───────────────────────────── */
.table-wrap {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: var(--radius);
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.table th,
.table td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
text-align: left;
vertical-align: top;
}
.table th {
background: var(--muted);
color: var(--muted-fg);
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.table tbody tr:hover { background: var(--primary-light); }
.table tbody tr:last-child td { border-bottom: none; }
/* ── Empty State ─────────────────────────────────────── */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
color: var(--muted-fg);
}
.empty-state .empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.25;
}
.empty-state h3 {
font-size: 15px;
font-weight: 600;
color: var(--foreground);
margin: 0 0 4px;
}
.empty-state p {
font-size: 13px;
margin: 0;
}
/* ── Tooltip ─────────────────────────────────────────── */
.tooltip-trigger { cursor: help; }
/* ── Separator ───────────────────────────────────────── */
.separator {
border: none;
border-top: 1px solid var(--border);
margin: 12px 0;
}
/* ── Animations ──────────────────────────────────────── */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-fade { animation: fadeIn 0.25s ease; }
.animate-slide { animation: slideIn 0.25s ease; }
/* ═══════════════════════════════════════════════════════════
Canifa AI Platform — Shared Theme Variables
Inspired by Memos (usememos.com) Light Theme
═══════════════════════════════════════════════════════════ */
:root {
/* ── Surfaces ── */
--background: #f8f7f4;
--foreground: #1c1917;
--card: #ffffff;
--card-foreground: #1c1917;
/* ── Muted / Secondary ── */
--muted: #f0efec;
--muted-fg: #78716c;
--secondary: #f0efec;
--secondary-fg: #57534e;
/* ── Primary (Calm Blue) ── */
--primary: #3b5998;
--primary-hover: #2d4a7a;
--primary-light: #eef1f8;
--primary-fg: #ffffff;
/* ── Accent ── */
--accent: #eef1f8;
--accent-fg: #1c1917;
/* ── Borders & Input ── */
--border: #e7e5e2;
--input: #d6d3ce;
--ring: #3b5998;
/* ── Sidebar ── */
--sidebar: #f2f0ed;
--sidebar-fg: #57534e;
--sidebar-accent: #e7e5e2;
--sidebar-accent-fg:#1c1917;
/* ── Semantic ── */
--success: #16a34a;
--success-light: #ecfdf5;
--error: #dc2626;
--error-light: #fef2f2;
--warn: #ca8a04;
--warn-light: #fefce8;
--info: #2563eb;
--info-light: #eff6ff;
/* ── Dimensions ── */
--radius: 0.5rem;
--radius-sm: 0.25rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* ── Shadows ── */
--shadow-xs: 0 1px 2px rgba(0,0,0,0.04);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
--shadow-md: 0 2px 8px rgba(0,0,0,0.06);
--shadow-lg: 0 4px 12px rgba(0,0,0,0.08);
/* ── Typography ── */
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
}
/* ── Global Reset ── */
*, *::before, *::after { box-sizing: border-box; }
/* ── Thin Scrollbar (Memos-style) ── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--input); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted-fg); }
/* ── Font Smoothing ── */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
......@@ -4,8 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chi tiết thử nghiệm - Canifa AI</title>
<link rel="stylesheet" href="/static/lab.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* { box-sizing: border-box; }
body {
......
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="vi">
<head>
......@@ -6,9 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa Chatbot - Feedback Demo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* {
margin: 0;
......@@ -483,9 +485,9 @@
<div class="chat-messages" id="chatMessages">
<!-- Message 1: User -->
<div class="msg-group user">
<span class="msg-label">Bn</span>
<span class="msg-label">B?n</span>
<div class="msg-bubble user">
Tìm cho tôi áo polo nam màu xanh navy, size L
Tm cho ti o polo nam mu xanh navy, size L
</div>
</div>
......@@ -493,22 +495,22 @@
<div class="msg-group bot" id="msg-1">
<span class="msg-label">Canifa AI</span>
<div class="msg-bubble bot">
Dạ, em tìm thấy 3 sản phẩm áo polo nam màu xanh navy size L cho anh ạ! 👕
D?, em tm th?y 3 s?n ph?m o polo nam mu xanh navy size L cho anh ?! ??
<div class="product-cards-mini">
<div class="product-mini">
<img src="https://media.canifa.com/Simiconnector/apicatalog_product_image/file/6/P/6PM24S001-SN010-thumb.webp"
alt="polo" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22140%22 height=%22100%22><rect fill=%22%23252540%22 width=%22140%22 height=%22100%22/><text x=%2250%25%22 y=%2250%25%22 fill=%22%23667eea%22 text-anchor=%22middle%22 dy=%22.3em%22 font-size=%2214%22>👕</text></svg>'">
alt="polo" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22140%22 height=%22100%22><rect fill=%22%23252540%22 width=%22140%22 height=%22100%22/><text x=%2250%25%22 y=%2250%25%22 fill=%22%23667eea%22 text-anchor=%22middle%22 dy=%22.3em%22 font-size=%2214%22>??</text></svg>'">
<div class="pm-info">
<div class="pm-name">Áo Polo Nam Cotton</div>
<div class="pm-price">299.000</div>
<div class="pm-name">o Polo Nam Cotton</div>
<div class="pm-price">299.000?</div>
</div>
</div>
<div class="product-mini">
<img src="https://media.canifa.com/Simiconnector/apicatalog_product_image/file/6/P/6PM24S002-SN010-thumb.webp"
alt="polo" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22140%22 height=%22100%22><rect fill=%22%23252540%22 width=%22140%22 height=%22100%22/><text x=%2250%25%22 y=%2250%25%22 fill=%22%23667eea%22 text-anchor=%22middle%22 dy=%22.3em%22 font-size=%2214%22>👕</text></svg>'">
alt="polo" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22140%22 height=%22100%22><rect fill=%22%23252540%22 width=%22140%22 height=%22100%22/><text x=%2250%25%22 y=%2250%25%22 fill=%22%23667eea%22 text-anchor=%22middle%22 dy=%22.3em%22 font-size=%2214%22>??</text></svg>'">
<div class="pm-info">
<div class="pm-name">Áo Polo Pique Co Giãn</div>
<div class="pm-price">349.000</div>
<div class="pm-name">o Polo Pique Co Gin</div>
<div class="pm-price">349.000?</div>
</div>
</div>
</div>
......@@ -516,37 +518,37 @@
<!-- FEEDBACK BUTTONS -->
<div class="feedback-area" id="fb-area-1">
<button class="fb-btn" onclick="handleLike('msg-1', this)" title="Hữu ích">
<span class="icon">👍</span> Hữu ích
<button class="fb-btn" onclick="handleLike('msg-1', this)" title="H?u ch">
<span class="icon">??</span> H?u ch
</button>
<button class="fb-btn" onclick="handleDislike('msg-1', this)" title="Không hữu ích">
<span class="icon">👎</span>
<button class="fb-btn" onclick="handleDislike('msg-1', this)" title="Khng h?u ch">
<span class="icon">??</span>
</button>
<button class="fb-btn-comment" onclick="toggleCommentForm('msg-1')" title="Phàn nàn / Góp ý">
<span class="icon">📝</span> Phàn nàn
<button class="fb-btn-comment" onclick="toggleCommentForm('msg-1')" title="Phn nn / Gp ">
<span class="icon">??</span> Phn nn
</button>
</div>
<!-- COMMENT FORM (hidden) -->
<div class="feedback-comment-form" id="comment-form-msg-1">
<div class="feedback-comment-inner">
<div class="fc-label">Chọn loại phản hồi:</div>
<div class="fc-label">Ch?n lo?i ph?n h?i:</div>
<div class="feedback-category-chips" id="chips-msg-1">
<button class="category-chip" onclick="selectChip(this, 'msg-1')">🔴 Thiếu dữ liệu
<button class="category-chip" onclick="selectChip(this, 'msg-1')">?? Thi?u d? li?u
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-1')">🟡 Dữ liệu sai
<button class="category-chip" onclick="selectChip(this, 'msg-1')">?? D? li?u sai
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-1')">🟣 Bot không hiểu
<button class="category-chip" onclick="selectChip(this, 'msg-1')">?? Bot khng hi?u
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-1')">⚪ Khác</button>
<button class="category-chip" onclick="selectChip(this, 'msg-1')">? Khc</button>
</div>
<textarea class="feedback-textarea" id="textarea-msg-1"
placeholder="Mô tả chi tiết vấn đề bạn gặp..."></textarea>
placeholder="M t? chi ti?t v?n d? b?n g?p..."></textarea>
<div class="feedback-submit-row">
<button class="fb-submit-btn fb-cancel-btn"
onclick="toggleCommentForm('msg-1')">Hy</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('msg-1')">Gửi phản
hi</button>
onclick="toggleCommentForm('msg-1')">H?y</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('msg-1')">G?i ph?n
h?i</button>
</div>
</div>
</div>
......@@ -557,9 +559,9 @@
<!-- Message 3: User -->
<div class="msg-group user">
<span class="msg-label">Bn</span>
<span class="msg-label">B?n</span>
<div class="msg-bubble user">
Giá áo khoác nữ dưới 500k?
Gi o khoc n? du?i 500k?
</div>
</div>
......@@ -567,14 +569,14 @@
<div class="msg-group bot" id="msg-2">
<span class="msg-label">Canifa AI</span>
<div class="msg-bubble bot">
Dạ, em tìm thấy áo khoác nữ cho chị ạ! Giá hiện tại là <b>599.000₫</b> 🧥
D?, em tm th?y o khoc n? cho ch? ?! Gi hi?n t?i l <b>599.000?</b> ??
<div class="product-cards-mini">
<div class="product-mini">
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='100'><rect fill='%23252540' width='140' height='100'/><text x='50%25' y='50%25' fill='%23667eea' text-anchor='middle' dy='.3em' font-size='14'>🧥</text></svg>"
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='100'><rect fill='%23252540' width='140' height='100'/><text x='50%25' y='50%25' fill='%23667eea' text-anchor='middle' dy='.3em' font-size='14'>??</text></svg>"
alt="jacket">
<div class="pm-info">
<div class="pm-name">Áo Khoác Gió Nữ</div>
<div class="pm-price">599.000</div>
<div class="pm-name">o Khoc Gi N?</div>
<div class="pm-price">599.000?</div>
</div>
</div>
</div>
......@@ -582,37 +584,37 @@
<!-- FEEDBACK BUTTONS -->
<div class="feedback-area" id="fb-area-2">
<button class="fb-btn" onclick="handleLike('msg-2', this)" title="Hữu ích">
<span class="icon">👍</span> Hữu ích
<button class="fb-btn" onclick="handleLike('msg-2', this)" title="H?u ch">
<span class="icon">??</span> H?u ch
</button>
<button class="fb-btn" onclick="handleDislike('msg-2', this)" title="Không hữu ích">
<span class="icon">👎</span>
<button class="fb-btn" onclick="handleDislike('msg-2', this)" title="Khng h?u ch">
<span class="icon">??</span>
</button>
<button class="fb-btn-comment" onclick="toggleCommentForm('msg-2')" title="Phàn nàn / Góp ý">
<span class="icon">📝</span> Phàn nàn
<button class="fb-btn-comment" onclick="toggleCommentForm('msg-2')" title="Phn nn / Gp ">
<span class="icon">??</span> Phn nn
</button>
</div>
<!-- COMMENT FORM (hidden) -->
<div class="feedback-comment-form" id="comment-form-msg-2">
<div class="feedback-comment-inner">
<div class="fc-label">Chọn loại phản hồi:</div>
<div class="fc-label">Ch?n lo?i ph?n h?i:</div>
<div class="feedback-category-chips" id="chips-msg-2">
<button class="category-chip" onclick="selectChip(this, 'msg-2')">🔴 Thiếu dữ liệu
<button class="category-chip" onclick="selectChip(this, 'msg-2')">?? Thi?u d? li?u
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-2')">🟡 Dữ liệu sai
<button class="category-chip" onclick="selectChip(this, 'msg-2')">?? D? li?u sai
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-2')">🟣 Bot không hiểu
<button class="category-chip" onclick="selectChip(this, 'msg-2')">?? Bot khng hi?u
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-2')">⚪ Khác</button>
<button class="category-chip" onclick="selectChip(this, 'msg-2')">? Khc</button>
</div>
<textarea class="feedback-textarea" id="textarea-msg-2"
placeholder="Mô tả chi tiết vấn đề bạn gặp..."></textarea>
placeholder="M t? chi ti?t v?n d? b?n g?p..."></textarea>
<div class="feedback-submit-row">
<button class="fb-submit-btn fb-cancel-btn"
onclick="toggleCommentForm('msg-2')">Hy</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('msg-2')">Gửi phản
hi</button>
onclick="toggleCommentForm('msg-2')">H?y</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('msg-2')">G?i ph?n
h?i</button>
</div>
</div>
</div>
......@@ -621,9 +623,9 @@
<!-- Message 5: User -->
<div class="msg-group user">
<span class="msg-label">Bn</span>
<span class="msg-label">B?n</span>
<div class="msg-bubble user">
Có quần jean nam size 32 không?
C qu?n jean nam size 32 khng?
</div>
</div>
......@@ -631,41 +633,41 @@
<div class="msg-group bot" id="msg-3">
<span class="msg-label">Canifa AI</span>
<div class="msg-bubble bot">
Dạ, hiện tại em tìm thấy 2 quần jean nam size 32 cho anh ạ! 👖 Cả 2 đều đang còn hàng tại cửa
hàng TP.HCM.
D?, hi?n t?i em tm th?y 2 qu?n jean nam size 32 cho anh ?! ?? C? 2 d?u dang cn hng t?i c?a
hng TP.HCM.
</div>
<div class="feedback-area" id="fb-area-3">
<button class="fb-btn" onclick="handleLike('msg-3', this)">
<span class="icon">👍</span> Hữu ích
<span class="icon">??</span> H?u ch
</button>
<button class="fb-btn" onclick="handleDislike('msg-3', this)">
<span class="icon">👎</span>
<span class="icon">??</span>
</button>
<button class="fb-btn-comment" onclick="toggleCommentForm('msg-3')">
<span class="icon">📝</span> Phàn nàn
<span class="icon">??</span> Phn nn
</button>
</div>
<div class="feedback-comment-form" id="comment-form-msg-3">
<div class="feedback-comment-inner">
<div class="fc-label">Chọn loại phản hồi:</div>
<div class="fc-label">Ch?n lo?i ph?n h?i:</div>
<div class="feedback-category-chips" id="chips-msg-3">
<button class="category-chip" onclick="selectChip(this, 'msg-3')">🔴 Thiếu dữ liệu
<button class="category-chip" onclick="selectChip(this, 'msg-3')">?? Thi?u d? li?u
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-3')">🟡 Dữ liệu sai
<button class="category-chip" onclick="selectChip(this, 'msg-3')">?? D? li?u sai
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-3')">🟣 Bot không hiểu
<button class="category-chip" onclick="selectChip(this, 'msg-3')">?? Bot khng hi?u
</button>
<button class="category-chip" onclick="selectChip(this, 'msg-3')">⚪ Khác</button>
<button class="category-chip" onclick="selectChip(this, 'msg-3')">? Khc</button>
</div>
<textarea class="feedback-textarea" id="textarea-msg-3"
placeholder="Mô tả chi tiết vấn đề bạn gặp..."></textarea>
placeholder="M t? chi ti?t v?n d? b?n g?p..."></textarea>
<div class="feedback-submit-row">
<button class="fb-submit-btn fb-cancel-btn"
onclick="toggleCommentForm('msg-3')">Hy</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('msg-3')">Gửi phản
hi</button>
onclick="toggleCommentForm('msg-3')">H?y</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('msg-3')">G?i ph?n
h?i</button>
</div>
</div>
</div>
......@@ -674,9 +676,9 @@
</div>
<div class="chat-input-bar">
<input type="text" placeholder="Nhập tin nhắn..." id="chatInput"
<input type="text" placeholder="Nh?p tin nh?n..." id="chatInput"
onkeypress="if(event.key==='Enter') sendDemo()">
<button class="send-btn" onclick="sendDemo()">Gửi ➤</button>
<button class="send-btn" onclick="sendDemo()">G?i ?</button>
</div>
</div>
</div>
......@@ -694,8 +696,8 @@
const form = document.getElementById('comment-form-' + msgId);
if (form) form.classList.remove('open');
showToast(msgId, '✅ Cảm ơn bạn đã đánh giá! Feedback đã được ghi nhận.');
console.log(`[Feedback] LIKE for ${msgId} → sẽ POST /api/feedback {trace_id, rating: 1}`);
showToast(msgId, '? C?m on b?n d dnh gi! Feedback d du?c ghi nh?n.');
console.log(`[Feedback] LIKE for ${msgId} ? s? POST /api/feedback {trace_id, rating: 1}`);
}
function handleDislike(msgId, btn) {
......@@ -735,23 +737,23 @@
const textarea = document.getElementById('textarea-' + msgId);
const comment = textarea ? textarea.value : '';
const selectedChip = document.querySelector(`#chips-${msgId} .category-chip.selected`);
const category = selectedChip ? selectedChip.textContent.trim() : 'Chưa chọn';
const category = selectedChip ? selectedChip.textContent.trim() : 'Chua ch?n';
console.log(`[Feedback] SUBMIT for ${msgId}:`);
console.log(` Category: ${category}`);
console.log(` Comment: ${comment}`);
console.log(` → sẽ POST /api/feedback {trace_id, rating: 0, category, comment}`);
console.log(` → AI Classifier sẽ phân loại → Langfuse score`);
console.log(` ? s? POST /api/feedback {trace_id, rating: 0, category, comment}`);
console.log(` ? AI Classifier s? phn lo?i ? Langfuse score`);
// Close form + show toast
const form = document.getElementById('comment-form-' + msgId);
form.classList.remove('open');
showToast(msgId, `✅ Phản hồi đã gửi! AI đang phân loại: "${category}"`);
showToast(msgId, `? Ph?n h?i d g?i! AI dang phn lo?i: "${category}"`);
// Simulate AI processing
setTimeout(() => {
showToast(msgId, `✅ AI đã phân loại: ${category} | Severity: HIGH | Đã ghi vào hệ thống ⏳`);
showToast(msgId, `? AI d phn lo?i: ${category} | Severity: HIGH | ghi vo h? th?ng ?`);
}, 2000);
}
......@@ -775,7 +777,7 @@
const userGroup = document.createElement('div');
userGroup.className = 'msg-group user';
userGroup.innerHTML = `
<span class="msg-label">Bn</span>
<span class="msg-label">B?n</span>
<div class="msg-bubble user">${input.value}</div>
`;
messages.appendChild(userGroup);
......@@ -788,32 +790,32 @@
botGroup.innerHTML = `
<span class="msg-label">Canifa AI</span>
<div class="msg-bubble bot">
Dạ, em đã nhận câu hỏi của anh/chị ạ! Đây là bản demo nên em chưa xử lý được, nhưng feedback sẽ hoạt động bình thường nhé 😊
D?, em d nh?n cu h?i c?a anh/ch? ?! y l b?n demo nn em chua x? l du?c, nhung feedback s? ho?t d?ng bnh thu?ng nh ??
</div>
<div class="feedback-area" id="fb-area-${msgId}">
<button class="fb-btn" onclick="handleLike('${msgId}', this)">
<span class="icon">👍</span> Hữu ích
<span class="icon">??</span> H?u ch
</button>
<button class="fb-btn" onclick="handleDislike('${msgId}', this)">
<span class="icon">👎</span>
<span class="icon">??</span>
</button>
<button class="fb-btn-comment" onclick="toggleCommentForm('${msgId}')">
<span class="icon">📝</span> Phàn nàn
<span class="icon">??</span> Phn nn
</button>
</div>
<div class="feedback-comment-form" id="comment-form-${msgId}">
<div class="feedback-comment-inner">
<div class="fc-label">Chọn loại phản hồi:</div>
<div class="fc-label">Ch?n lo?i ph?n h?i:</div>
<div class="feedback-category-chips" id="chips-${msgId}">
<button class="category-chip" onclick="selectChip(this, '${msgId}')">🔴 Thiếu dữ liệu</button>
<button class="category-chip" onclick="selectChip(this, '${msgId}')">🟡 Dữ liệu sai</button>
<button class="category-chip" onclick="selectChip(this, '${msgId}')">🟣 Bot không hiểu</button>
<button class="category-chip" onclick="selectChip(this, '${msgId}')">⚪ Khác</button>
<button class="category-chip" onclick="selectChip(this, '${msgId}')">?? Thi?u d? li?u</button>
<button class="category-chip" onclick="selectChip(this, '${msgId}')">?? D? li?u sai</button>
<button class="category-chip" onclick="selectChip(this, '${msgId}')">?? Bot khng hi?u</button>
<button class="category-chip" onclick="selectChip(this, '${msgId}')">? Khc</button>
</div>
<textarea class="feedback-textarea" id="textarea-${msgId}" placeholder="Mô tả chi tiết vấn đề bạn gặp..."></textarea>
<textarea class="feedback-textarea" id="textarea-${msgId}" placeholder="M t? chi ti?t v?n d? b?n g?p..."></textarea>
<div class="feedback-submit-row">
<button class="fb-submit-btn fb-cancel-btn" onclick="toggleCommentForm('${msgId}')">Hy</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('${msgId}')">Gửi phản hồi</button>
<button class="fb-submit-btn fb-cancel-btn" onclick="toggleCommentForm('${msgId}')">H?y</button>
<button class="fb-submit-btn fb-send-btn" onclick="submitFeedback('${msgId}')">G?i ph?n h?i</button>
</div>
</div>
</div>
......
......@@ -7,8 +7,10 @@
<title>Sơ đồ hoạt động - Canifa AI</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="/static/lab.css"> <script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/flow.css">
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css"> <script src="/static/js/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/flow.css">
</head>
<body>
......
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hướng dẫn sử dụng - Canifa AI System</title>
<title>Hu?ng d?n s? d?ng - Canifa AI System</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: var(--bg, #FAF6F0); color: var(--t, #2C1810); min-height: 100vh; display: flex; }
/* ═══ SIDEBAR ═══ */
/* --- SIDEBAR --- */
.sidebar { width: 260px; min-height: 100vh; background: var(--s, #FFFFFF); border-right: 1px solid var(--b, #E8DED0); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; z-index: 100; }
.sidebar-brand { padding: 24px 20px; border-bottom: 1px solid var(--b, #E8DED0); display: flex; align-items: center; gap: 12px; }
.brand-icon { width: 40px; height: 40px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.3em; flex-shrink: 0; }
......@@ -34,14 +36,14 @@
.version-dot { width: 8px; height: 8px; border-radius: 50%; background: #56d364; box-shadow: 0 0 8px rgba(86,211,100,0.4); }
.version-text { font-size: 0.75em; color: var(--m, #6B5B4F); }
/* ═══ MAIN ═══ */
/* --- MAIN --- */
.main { margin-left: 260px; flex: 1; min-height: 100vh; display: flex; flex-direction: column; }
.topbar { padding: 24px 32px; border-bottom: 1px solid var(--b, #E8DED0); }
.topbar h1 { font-size: 1.5em; font-weight: 800; color: var(--t, #2C1810); }
.topbar p { font-size: 0.85em; color: #484f58; margin-top: 2px; }
.content { padding: 24px 32px; flex: 1; max-width: 900px; }
/* ═══ GUIDE ═══ */
/* --- GUIDE --- */
.section { margin-bottom: 36px; }
.section h2 { font-size: 1.1em; font-weight: 700; color: var(--t, #2C1810); margin-bottom: 8px; }
.section .desc { font-size: 0.84em; color: var(--m, #6B5B4F); line-height: 1.6; margin-bottom: 16px; }
......@@ -66,104 +68,104 @@
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">🤖</div>
<div class="brand-icon">??</div>
<div class="brand-text"><h2>Canifa AI</h2><span>Admin Console</span></div>
</div>
<div class="nav-group">
<div class="nav-group-label">Main</div>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">🔀</span><span>Sơ đồ hoạt động</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">💬</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">🧾</span><span>History</span></a>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">??</span><span>So d? ho?t d?ng</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">??</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">??</span><span>History</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Workspace</div>
<a href="/static/resources.html" class="nav-item"><span class="nav-icon">🔗</span><span>Resources</span></a>
<a href="/static/notes.html" class="nav-item"><span class="nav-icon">📝</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item"><span class="nav-icon">📋</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item active"><span class="nav-icon">📖</span><span>Hướng dẫn</span></a>
<a href="/static/resources.html" class="nav-item"><span class="nav-icon">??</span><span>Resources</span></a>
<a href="/static/notes.html" class="nav-item"><span class="nav-icon">??</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item"><span class="nav-icon">??</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item active"><span class="nav-icon">??</span><span>Hu?ng d?n</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Thử nghiệm</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">🗄️</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">🔍</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">📝</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
<div class="nav-group-label">Th? nghi?m</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">???</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">??</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">??</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">External</div>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">📚</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">📖</span><span>ReDoc</span></a>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>ReDoc</span></a>
</div>
<div class="sidebar-footer">
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> · Online</span></div>
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> Online</span></div>
</div>
</aside>
<div class="main">
<div class="topbar">
<h1>📖 Hướng dẫn sử dụng Workspace</h1>
<p>Cách dùng Resources, Team Notes và Changelog</p>
<h1>?? Hu?ng d?n s? d?ng Workspace</h1>
<p>Cch dng Resources, Team Notes v Changelog</p>
</div>
<div class="content">
<!-- ═══ RESOURCES ═══ -->
<!-- --- RESOURCES --- -->
<div class="section">
<h2>🔗 Resources — Bookmark tools & services</h2>
<p class="desc">Lưu tập trung tất cả link tools, services, tài liệu mà team đang sử dụng — thay vì nhắn qua Zalo rồi mất.</p>
<h2>?? Resources Bookmark tools & services</h2>
<p class="desc">Luu t?p trung t?t c? link tools, services, ti li?u m team dang s? d?ng thay v nh?n qua Zalo r?i m?t.</p>
<div class="card">
<h3>📂 Các category</h3>
<h3>?? Cc category</h3>
<table class="tbl">
<tr><th>Category</th><th>Chứa gì</th><th>Ví dụ</th></tr>
<tr><td>🔧 Tools</td><td>Link tools nội bộ đang chạy</td><td>Langfuse, LiteLLM, n8n, Portainer, Uptime Kuma, Meilisearch</td></tr>
<tr><td>📄 Docs</td><td>Tài liệu kỹ thuật</td><td>ReDoc, PRD, kiến trúc hệ thống</td></tr>
<tr><td>🔌 APIs</td><td>Endpoints / connections</td><td>Swagger UI, Redis connection string</td></tr>
<tr><td>🎨 Design</td><td>Link thiết kế, mockup</td><td>Figma, UI reference, wireframe</td></tr>
<tr><td>📦 Repos</td><td>Source code repositories</td><td>GitHub repos, branches chính</td></tr>
<tr><th>Category</th><th>Ch?a g</th><th>V d?</th></tr>
<tr><td>?? Tools</td><td>Link tools n?i b? dang ch?y</td><td>Langfuse, LiteLLM, n8n, Portainer, Uptime Kuma, Meilisearch</td></tr>
<tr><td>?? Docs</td><td>Ti li?u k? thu?t</td><td>ReDoc, PRD, ki?n trc h? th?ng</td></tr>
<tr><td>?? APIs</td><td>Endpoints / connections</td><td>Swagger UI, Redis connection string</td></tr>
<tr><td>?? Design</td><td>Link thi?t k?, mockup</td><td>Figma, UI reference, wireframe</td></tr>
<tr><td>?? Repos</td><td>Source code repositories</td><td>GitHub repos, branches chnh</td></tr>
</table>
<div class="tip">
<strong>💡 Cách dùng:</strong> Bấm <strong>+ Add Link</strong> để thêm. Bấm 📌 để ghim link lên đầu. Dùng filter bar để lọc theo category.
<strong>?? Cch dng:</strong> B?m <strong>+ Add Link</strong> d? thm. B?m ?? d? ghim link ln d?u. Dng filter bar d? l?c theo category.
</div>
</div>
</div>
<hr class="divider">
<!-- ═══ TEAM NOTES ═══ -->
<!-- --- TEAM NOTES --- -->
<div class="section">
<h2>📝 Team Notes — Ghi chú tập trung cho team</h2>
<p class="desc">Ghi chú cho cả team — to-do, bug, ý tưởng, meeting notes. Thay vì nhắn qua Zalo rồi trôi mất.</p>
<h2>?? Team Notes Ghi ch t?p trung cho team</h2>
<p class="desc">Ghi ch cho c? team to-do, bug, tu?ng, meeting notes. Thay v nh?n qua Zalo r?i tri m?t.</p>
<div class="card">
<h3>📂 Các loại note</h3>
<h3>?? Cc lo?i note</h3>
<table class="tbl">
<tr><th>Category</th><th>Ghi gì</th><th>Ví dụ</th></tr>
<tr><td>📝 Note</td><td>Ghi chú chung</td><td>"Redis restart mỗi đêm 00:00, cache sẽ bị xóa"</td></tr>
<tr><td>📄 Doc</td><td>Tài liệu nội bộ</td><td>"Cách deploy chatbot lên staging"</td></tr>
<tr><td>✅ To-do</td><td>Việc cần làm</td><td>"Tuần này: test load 100 users"</td></tr>
<tr><td>📢 Announcement</td><td>Thông báo team</td><td>"Deploy v2.5 ngày 15/3 — freeze code từ 14/3"</td></tr>
<tr><th>Category</th><th>Ghi g</th><th>V d?</th></tr>
<tr><td>?? Note</td><td>Ghi ch chung</td><td>"Redis restart m?i dm 00:00, cache s? b? xa"</td></tr>
<tr><td>?? Doc</td><td>Ti li?u n?i b?</td><td>"Cch deploy chatbot ln staging"</td></tr>
<tr><td>? To-do</td><td>Vi?c c?n lm</td><td>"Tu?n ny: test load 100 users"</td></tr>
<tr><td>?? Announcement</td><td>Thng bo team</td><td>"Deploy v2.5 ngy 15/3 freeze code t? 14/3"</td></tr>
</table>
<div class="tip">
<strong>💡 Cách dùng:</strong> Bấm <strong>+ New Note</strong> để tạo. Chọn <strong>màu</strong> khác nhau để phân biệt loại. Bấm 📌 để ghim lên đầu.
<strong>?? Cch dng:</strong> B?m <strong>+ New Note</strong> d? t?o. Ch?n <strong>mu</strong> khc nhau d? phn bi?t lo?i. B?m ?? d? ghim ln d?u.
</div>
</div>
</div>
<hr class="divider">
<!-- ═══ CHANGELOG ═══ -->
<!-- --- CHANGELOG --- -->
<div class="section">
<h2>📋 Changelog — Lịch sử thay đổi</h2>
<p class="desc">Ghi lại lịch sử thay đổi chatbot — ai sửa gì, khi nào. Dễ trace bug, dễ rollback.</p>
<h2>?? Changelog L?ch s? thay d?i</h2>
<p class="desc">Ghi l?i l?ch s? thay d?i chatbot ai s?a g, khi no. D? trace bug, d? rollback.</p>
<div class="card">
<h3>📝 Nên ghi gì?</h3>
<h3>?? Nn ghi g?</h3>
<table class="tbl">
<tr><th>Loại thay đổi</th><th>Ví dụ cách ghi</th></tr>
<tr><td>🔧 Fix bug</td><td>"Fix search 'áo bra' — thêm mapping skort"</td></tr>
<tr><td>📝 Sửa prompt</td><td>"Update system prompt v2.3 — thêm rule giới hạn emoji"</td></tr>
<tr><td>🚀 Deploy</td><td>"Deploy Gemini Flash Lite — giảm 55% cost"</td></tr>
<tr><td>📦 Thêm data</td><td>"Thêm 200 sản phẩm mới vào Meilisearch index"</td></tr>
<tr><td>⚙️ Config</td><td>"Tăng timeout Redis từ 5s → 10s"</td></tr>
<tr><th>Lo?i thay d?i</th><th>V d? cch ghi</th></tr>
<tr><td>?? Fix bug</td><td>"Fix search 'o bra' thm mapping skort"</td></tr>
<tr><td>?? S?a prompt</td><td>"Update system prompt v2.3 thm rule gi?i h?n emoji"</td></tr>
<tr><td>?? Deploy</td><td>"Deploy Gemini Flash Lite gi?m 55% cost"</td></tr>
<tr><td>?? Thm data</td><td>"Thm 200 s?n ph?m m?i vo Meilisearch index"</td></tr>
<tr><td>?? Config</td><td>"Tang timeout Redis t? 5s ? 10s"</td></tr>
</table>
<div class="tip">
<strong>💡 Cách dùng:</strong> Ghi <strong>tên bạn</strong> ở ô đầu tiên, mô tả thay đổi ở ô thứ hai. Nhấn <strong>Enter</strong> để thêm nhanh.
<strong>?? Cch dng:</strong> Ghi <strong>tn b?n</strong> ? d?u tin, m t? thay d?i ? th? hai. Nh?n <strong>Enter</strong> d? thm nhanh.
</div>
</div>
</div>
......
<!DOCTYPE html>
<html lang="en">
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa History Viewer</title>
<link rel="stylesheet" href="/static/lab.css">
<script src="/static/frame-detect.js"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>History Viewer — Canifa AI</title>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--t, #18181B);
background:
radial-gradient(circle at top right, rgba(180, 83, 9, 0.10), transparent 28%),
linear-gradient(180deg, #F7F4EE 0%, #F3EFE6 100%);
font-family: 'Outfit', sans-serif;
}
.page-shell {
width: min(960px, calc(100vw - 32px));
margin: 0 auto;
padding: 28px 0 36px;
}
.card {
background: rgba(255, 255, 255, 0.86);
border: 1px solid rgba(165, 142, 112, 0.18);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(24, 24, 27, 0.06);
backdrop-filter: blur(12px);
}
/* ═══ HERO ═══ */
.hero {
padding: 28px 30px;
position: relative;
overflow: hidden;
margin-bottom: 18px;
}
.hero::after {
content: "";
position: absolute;
inset: auto -40px -60px auto;
width: 220px; height: 220px;
border-radius: 999px;
background: radial-gradient(circle, rgba(180, 83, 9, 0.14), transparent 68%);
pointer-events: none;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
border: 1px solid rgba(180, 83, 9, 0.24);
background: rgba(255, 251, 235, 0.9);
color: #9A5D0B;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.hero-title {
margin: 16px 0 10px;
font-size: clamp(30px, 4vw, 42px);
line-height: 1;
letter-spacing: -0.05em;
font-family: 'Fraunces', serif;
}
.hero-copy {
max-width: 760px;
margin: 0;
color: var(--m, #78716C);
font-size: 15px;
line-height: 1.7;
}
/* ═══ COMPOSER ═══ */
.composer {
margin-bottom: 16px;
padding: 20px 24px;
}
.composer-grid {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.composer label {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #9A5D0B;
flex-shrink: 0;
}
.composer input[type="text"] {
flex: 1;
min-width: 200px;
border: 1px solid rgba(165, 142, 112, 0.18);
border-radius: 14px;
background: rgba(255,255,255,0.92);
padding: 12px 16px;
font: inherit;
font-size: 14px;
color: var(--t, #18181B);
outline: none;
}
.composer input[type="text"]:focus {
border-color: rgba(180, 83, 9, 0.45);
box-shadow: 0 0 0 4px rgba(180, 83, 9, 0.08);
}
.run-btn {
border: none;
border-radius: 14px;
padding: 12px 22px;
background: linear-gradient(135deg, #111827 0%, #374151 100%);
color: white;
font: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: transform .18s ease, box-shadow .18s ease;
box-shadow: 0 8px 20px rgba(17, 24, 39, 0.14);
}
.run-btn:hover { transform: translateY(-1px); }
.ghost-btn {
border: 1px solid rgba(165, 142, 112, 0.16);
background: rgba(255,255,255,0.78);
color: var(--t, #18181B);
border-radius: 14px;
padding: 12px 18px;
font: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all .16s ease;
}
.ghost-btn:hover {
border-color: rgba(180, 83, 9, 0.35);
background: rgba(255, 251, 235, 0.6);
}
/* ═══ CHAT BOX ═══ */
.chat-panel {
padding: 0;
display: flex;
flex-direction: column;
min-height: 60vh;
max-height: 70vh;
}
.chat-box {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
.chat-footer {
padding: 14px 24px;
border-top: 1px solid rgba(165, 142, 112, 0.14);
display: flex;
justify-content: space-between;
align-items: center;
}
.hint {
color: var(--m, #78716C);
font-size: 12px;
line-height: 1.6;
}
.error-text {
color: #B91C1C;
font-size: 13px;
}
/* ═══ MESSAGES ═══ */
.message-container {
display: flex;
flex-direction: column;
max-width: 80%;
}
.message-container.user {
align-self: flex-end;
align-items: flex-end;
}
.message-container.bot {
align-self: flex-start;
align-items: flex-start;
}
.sender-name {
font-size: 11px;
margin-bottom: 4px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--m, #78716C);
margin-left: 4px;
margin-right: 4px;
}
.message {
padding: 14px 18px;
border-radius: 18px;
line-height: 1.65;
word-wrap: break-word;
position: relative;
font-size: 14px;
}
.message.user {
background: linear-gradient(135deg, #111827 0%, #374151 100%);
color: white;
border-bottom-right-radius: 4px;
}
.message.bot {
background: rgba(250, 247, 241, 0.92);
color: var(--t, #18181B);
border-bottom-left-radius: 4px;
border: 1px solid rgba(165, 142, 112, 0.16);
}
.message.system {
background: rgba(185, 28, 28, 0.06);
color: #B91C1C;
align-self: center;
font-size: 13px;
max-width: 90%;
border: 1px solid rgba(185, 28, 28, 0.14);
border-radius: 14px;
}
.message-meta {
display: flex;
gap: 8px;
font-size: 11px;
margin-top: 8px;
}
.message-meta span {
background: rgba(250, 245, 236, 0.92);
padding: 3px 8px;
border-radius: 8px;
border: 1px solid rgba(165, 142, 112, 0.12);
color: var(--m, #78716C);
}
.message.user .message-meta span {
background: rgba(255,255,255,0.15);
border-color: transparent;
color: rgba(255,255,255,0.7);
}
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(250, 245, 236, 0.92);
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
color: var(--t, #18181B);
border: 1px solid rgba(165, 142, 112, 0.14);
}
.raw-json {
margin-top: 10px;
background: #151821;
border: none;
padding: 14px;
border-radius: 14px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11px;
white-space: pre-wrap;
word-break: break-word;
color: #E5E7EB;
line-height: 1.6;
}
.load-more-btn {
padding: 12px 20px;
background: rgba(250, 247, 241, 0.92);
color: var(--m, #78716C);
border: 1px dashed rgba(165, 142, 112, 0.22);
border-radius: 14px;
cursor: pointer;
font: inherit;
font-size: 13px;
font-weight: 600;
margin: 0 auto;
display: none;
transition: all 0.2s;
width: 100%;
text-align: center;
}
.load-more-btn:hover {
background: rgba(255, 251, 235, 0.8);
border-color: rgba(180, 83, 9, 0.3);
color: #9A5D0B;
}
.load-more-btn.visible { display: block; }
@media (max-width: 720px) {
.page-shell { width: min(100vw - 20px, 100%); padding-top: 18px; }
.hero, .composer { padding: 16px; }
.composer-grid { flex-direction: column; align-items: stretch; }
.chat-box { padding: 14px; }
}
body { margin:0; min-height:100vh; background:var(--bg); font-family:var(--font-sans); color:var(--foreground); }
.page { max-width:900px; margin:0 auto; padding:20px 20px 60px; }
/* Header */
.page-hdr { display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; }
.page-hdr h1 { font-size:20px; font-weight:700; margin:0; }
.page-hdr p { font-size:13px; color:var(--muted-fg); margin:2px 0 0; }
/* Filter Bar */
.filter-bar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:14px 16px; background:var(--card); border:1px solid var(--border); border-radius:10px; margin-bottom:14px; }
.filter-bar label { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--muted-fg); }
.filter-bar input[type="text"], .filter-bar input[type="date"] {
padding:8px 12px; border:1px solid var(--border); border-radius:8px; font:inherit; font-size:13px; background:var(--card); color:var(--foreground); outline:none;
}
.filter-bar input:focus { border-color:var(--primary); box-shadow:0 0 0 3px rgba(59,89,152,0.08); }
.filter-bar .input-key { flex:1; min-width:200px; }
.filter-actions { display:flex; gap:6px; }
/* Stats */
.stats-bar { display:flex; gap:16px; padding:8px 0; font-size:12px; color:var(--muted-fg); margin-bottom:10px; }
.stats-bar .stat { display:flex; align-items:center; gap:4px; }
.stats-bar .stat i { font-size:11px; }
/* Chat */
.chat-panel { background:var(--card); border:1px solid var(--border); border-radius:10px; display:flex; flex-direction:column; min-height:50vh; max-height:72vh; }
.chat-scroll { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:10px; }
.chat-empty { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--muted-fg); text-align:center; gap:8px; }
.chat-empty i { font-size:40px; opacity:.2; }
.chat-empty p { font-size:13px; }
/* Messages */
.msg { display:flex; flex-direction:column; max-width:82%; }
.msg.user { align-self:flex-end; align-items:flex-end; }
.msg.bot { align-self:flex-start; align-items:flex-start; }
.msg-sender { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--muted-fg); margin:0 6px 3px; }
.msg-bubble { padding:12px 16px; border-radius:16px; line-height:1.6; font-size:13.5px; word-wrap:break-word; white-space: pre-wrap; }
.msg.user .msg-bubble { background:var(--foreground); color:white; border-bottom-right-radius:4px; }
.msg.bot .msg-bubble { background:var(--bg); border:1px solid var(--border); border-bottom-left-radius:4px; }
.msg-time { font-size:10px; color:var(--muted-fg); margin:3px 6px 0; display:flex; gap:8px; align-items:center; }
.msg-time .raw-toggle { cursor:pointer; text-decoration:underline; color:var(--primary); }
.msg-time .raw-toggle:hover { opacity:.7; }
.msg-raw { background:#1e1e2e; color:#cdd6f4; font-family:var(--font-mono,monospace); font-size:11px; padding:10px 12px; border-radius:8px; margin-top:6px; white-space:pre-wrap; word-break:break-all; display:none; max-height:200px; overflow-y:auto; }
.msg-raw.open { display:block; }
/* Product Cards */
.product-row { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
.product-card { display:flex; gap:8px; padding:8px 10px; background:var(--card); border:1px solid var(--border); border-radius:8px; max-width:280px; min-width:200px; transition:box-shadow .15s; }
.product-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.06); }
.product-card img { width:48px; height:48px; border-radius:6px; object-fit:cover; flex-shrink:0; background:var(--muted); }
.product-info { flex:1; min-width:0; }
.product-name { font-size:12px; font-weight:600; line-height:1.3; overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
.product-price { font-size:11px; margin-top:2px; }
.product-price .sale { color:var(--error,#dc2626); font-weight:700; }
.product-price .original { color:var(--muted-fg); text-decoration:line-through; margin-left:4px; font-size:10px; }
.product-price .normal { color:var(--foreground); font-weight:600; }
.product-link { font-size:10px; color:var(--primary); margin-top:2px; display:block; text-decoration:none; }
.product-link:hover { text-decoration:underline; }
/* Load more */
.load-more { width:100%; padding:10px; background:var(--bg); border:1px dashed var(--border); border-radius:8px; cursor:pointer; font:inherit; font-size:12px; font-weight:600; color:var(--muted-fg); text-align:center; display:none; }
.load-more:hover { border-color:var(--primary); color:var(--primary); }
.load-more.visible { display:block; }
</style>
</head>
<body>
<div class="page">
<div class="page-hdr">
<div>
<h1>💬 History Viewer</h1>
<p>Xem lịch sử hội thoại theo identity key</p>
</div>
</div>
<div class="page-shell">
<div class="hero card">
<div class="eyebrow">History</div>
<h1 class="hero-title">History Viewer</h1>
<p class="hero-copy">Hiển thị lịch sử hội thoại theo identity_key (device_id hoặc user_id). Dữ liệu raw từ database.</p>
</div>
<div class="composer card">
<div class="composer-grid">
<label>Identity Key</label>
<input type="text" id="identityKey" placeholder="device_id hoặc user_id...">
<button class="run-btn" onclick="loadHistory()">Fetch (20)</button>
<button class="ghost-btn" onclick="clearResults()">Clear</button>
<!-- Filter Bar -->
<div class="filter-bar">
<label>Identity Key</label>
<input type="text" class="input-key" id="identityKey" placeholder="device_id hoặc user_id...">
<label>Từ</label>
<input type="date" id="dateFrom">
<label>Đến</label>
<input type="date" id="dateTo">
<div class="filter-actions">
<button class="btn btn-primary btn-sm" onclick="loadHistory()">Fetch</button>
<button class="btn btn-outline btn-sm" onclick="setToday()">Hôm nay</button>
<button class="btn btn-outline btn-sm" onclick="clearResults()">Clear</button>
</div>
</div>
</div>
<div class="chat-panel card">
<div class="chat-box" id="chatBox">
<button id="loadMoreBtn" class="load-more-btn" onclick="loadMore()">Load Older Messages</button>
<div id="messagesArea" style="display: flex; flex-direction: column; gap: 15px;"></div>
<!-- Stats -->
<div class="stats-bar" id="statsBar" style="display:none;">
<div class="stat"><i class="fas fa-comment"></i> <span id="statMessages">0</span> messages</div>
<div class="stat"><i class="fas fa-box"></i> <span id="statProducts">0</span> sản phẩm</div>
<div class="stat"><i class="fas fa-calendar"></i> <span id="statDateRange"></span></div>
</div>
<div class="chat-footer">
<div class="hint">Tip: nhập identity key rồi bấm Fetch để xem lịch sử.</div>
<div class="error-text" id="errorBox"></div>
<!-- Chat Panel -->
<div class="chat-panel">
<div class="chat-scroll" id="chatScroll">
<button id="loadMoreBtn" class="load-more" onclick="loadMore()">⬆ Load tin nhắn cũ hơn</button>
<div id="messagesArea">
<div class="chat-empty" id="emptyState">
<i class="fas fa-comments"></i>
<p>Nhập identity key và nhấn <strong>Fetch</strong> để xem lịch sử</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const messagesArea = document.getElementById('messagesArea');
const errorBox = document.getElementById('errorBox');
const loadMoreBtn = document.getElementById('loadMoreBtn');
let currentIdentityKey = '';
let nextCursor = null;
function clearResults() {
messagesArea.innerHTML = '';
errorBox.textContent = '';
currentIdentityKey = '';
nextCursor = null;
loadMoreBtn.classList.remove('visible');
}
function renderSystemMessage(text) {
const msg = document.createElement('div');
msg.className = 'message system';
msg.textContent = text;
messagesArea.appendChild(msg);
<script>
const messagesArea = document.getElementById('messagesArea');
const loadMoreBtn = document.getElementById('loadMoreBtn');
const emptyState = document.getElementById('emptyState');
let currentIdentityKey = '';
let nextCursor = null;
let totalMessages = 0;
let totalProducts = 0;
// Set default date to today
function setToday() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('dateFrom').value = today;
document.getElementById('dateTo').value = today;
}
function clearResults() {
messagesArea.innerHTML = '';
messagesArea.appendChild(emptyState);
emptyState.style.display = '';
currentIdentityKey = '';
nextCursor = null;
totalMessages = 0;
totalProducts = 0;
loadMoreBtn.classList.remove('visible');
document.getElementById('statsBar').style.display = 'none';
}
function formatTime(ts) {
if (!ts) return '';
const d = new Date(ts);
return d.toLocaleString('vi-VN', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' });
}
function formatPrice(p) {
if (!p) return '';
return Number(p).toLocaleString('vi-VN') + '₫';
}
function renderProductCards(products) {
if (!products || !products.length) return '';
const cards = products.map(p => {
const name = (typeof p === 'object') ? (p.name || p.sku || '') : String(p);
const sku = (typeof p === 'object') ? (p.sku || '') : String(p);
const thumb = (typeof p === 'object') ? (p.thumbnail_image_url || '') : '';
const price = (typeof p === 'object') ? p.price : null;
const salePrice = (typeof p === 'object') ? p.sale_price : null;
const url = (typeof p === 'object') ? p.url : '';
const imgSrc = thumb || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect fill="%23f0efec" width="48" height="48" rx="6"/><text x="24" y="28" text-anchor="middle" fill="%2378716c" font-size="10">📦</text></svg>';
let priceHtml = '';
if (salePrice && salePrice < price) {
priceHtml = `<span class="sale">${formatPrice(salePrice)}</span><span class="original">${formatPrice(price)}</span>`;
} else if (price) {
priceHtml = `<span class="normal">${formatPrice(price)}</span>`;
}
function renderMessage(item) {
const container = document.createElement('div');
container.className = `message-container ${item.is_human ? 'user' : 'bot'}`;
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.textContent = item.is_human ? 'USER' : 'AI';
const bubble = document.createElement('div');
bubble.className = `message ${item.is_human ? 'user' : 'bot'}`;
bubble.textContent = item.message || '';
const meta = document.createElement('div');
meta.className = 'message-meta';
const time = document.createElement('span');
time.textContent = item.timestamp || '';
const idChip = document.createElement('span');
idChip.textContent = `id=${item.id ?? ''}`;
meta.appendChild(time);
meta.appendChild(idChip);
bubble.appendChild(meta);
container.appendChild(sender);
container.appendChild(bubble);
if (!item.is_human && item.product_ids && item.product_ids.length) {
const products = document.createElement('div');
products.className = 'message bot';
products.style.marginTop = '6px';
products.textContent = `product_ids: ${JSON.stringify(item.product_ids, null, 2)}`;
container.appendChild(products);
}
const raw = document.createElement('div');
raw.className = 'raw-json';
raw.textContent = JSON.stringify(item, null, 2);
container.appendChild(raw);
const linkHtml = url ? `<a class="product-link" href="https://canifa.com/${url}" target="_blank">Xem SP ↗</a>` : '';
return `<div class="product-card">
<img src="${imgSrc}" alt="${name}" onerror="this.style.display='none'">
<div class="product-info">
<div class="product-name" title="${name}">${name || sku}</div>
<div class="product-price">${priceHtml}</div>
${linkHtml}
</div>
</div>`;
}).join('');
return `<div class="product-row">${cards}</div>`;
}
function createMessageEl(item, rawData) {
const isUser = item.is_human;
const div = document.createElement('div');
div.className = `msg ${isUser ? 'user' : 'bot'}`;
const products = (!isUser && item.product_ids && item.product_ids.length) ? item.product_ids : [];
totalProducts += products.length;
const rawId = 'raw_' + (item.id || Math.random().toString(36).substr(2,6));
div.innerHTML = `
<div class="msg-sender">${isUser ? '👤 Khách hàng' : '🤖 AI'}</div>
<div class="msg-bubble">${escapeHtml(item.message || '')}</div>
${!isUser ? renderProductCards(products) : ''}
<div class="msg-time">
${formatTime(item.timestamp)}
<span class="raw-toggle" onclick="document.getElementById('${rawId}').classList.toggle('open')">Raw</span>
</div>
<div class="msg-raw" id="${rawId}">${JSON.stringify(rawData || item, null, 2)}</div>
`;
return div;
}
messagesArea.appendChild(container);
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
async function loadHistory() {
clearResults();
const identityKey = document.getElementById('identityKey').value.trim();
async function loadHistory() {
clearResults();
const key = document.getElementById('identityKey').value.trim();
if (!key) { alert('Nhập identity key trước!'); return; }
currentIdentityKey = key;
await fetchMessages();
}
if (!identityKey) {
errorBox.textContent = 'Vui lòng nhập identity key.';
return;
}
async function loadMore() {
if (!nextCursor || !currentIdentityKey) return;
await fetchMessages(nextCursor);
}
currentIdentityKey = identityKey;
await fetchMessages();
async function fetchMessages(beforeId = null) {
const params = new URLSearchParams({ limit: '30' });
if (beforeId) params.append('before_id', beforeId);
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
if (dateFrom) params.append('date_from', dateFrom);
if (dateTo) params.append('date_to', dateTo);
try {
const res = await fetch(`/api/check-history/${encodeURIComponent(currentIdentityKey)}?${params.toString()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const payload = await res.json();
const items = payload.data || [];
if (!items.length && !beforeId) {
emptyState.innerHTML = '<i class="fas fa-search"></i><p>Không tìm thấy lịch sử cho identity key này</p>';
return;
}
async function loadMore() {
if (!nextCursor || !currentIdentityKey) return;
await fetchMessages(nextCursor);
}
emptyState.style.display = 'none';
async function fetchMessages(beforeId = null) {
const params = new URLSearchParams({ limit: '20' });
// Items come newest-first, reverse for display
const reversed = [...items].reverse();
reversed.forEach(item => {
if (beforeId) {
params.append('before_id', beforeId);
}
try {
const res = await fetch(`/api/check-history/${encodeURIComponent(currentIdentityKey)}?${params.toString()}`);
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
const payload = await res.json();
const items = payload.data || [];
if (!items.length) {
if (!beforeId) {
renderSystemMessage('No history found. Start chatting!');
}
loadMoreBtn.classList.remove('visible');
nextCursor = null;
return;
}
// Reverse to show oldest first when appending to top
items.reverse().forEach((item) => {
if (beforeId) {
// Prepend older messages to top
messagesArea.insertBefore(createMessageElement(item), messagesArea.firstChild);
} else {
// Initial load - append normally
renderMessage(item);
}
});
nextCursor = payload.next_cursor;
if (nextCursor) {
loadMoreBtn.classList.add('visible');
} else {
loadMoreBtn.classList.remove('visible');
}
} catch (err) {
errorBox.textContent = `Li: ${err.message || err}`;
}
}
function createMessageElement(item) {
const container = document.createElement('div');
container.className = 'message-container';
container.onclick = function (e) {
if (e.target === container || e.target.classList.contains('message')) return;
const rawDiv = container.querySelector('.raw-json');
if (rawDiv) rawDiv.classList.toggle('show');
};
const sender = document.createElement('div');
sender.className = 'sender-name';
sender.innerHTML = item.is_human
? '<span class="pill">Khách hàng</span>'
: '<span class="pill">Bot</span>';
const bubble = document.createElement('div');
bubble.className = 'message ' + (item.is_human ? 'user' : 'bot');
bubble.textContent = item.content || '';
const meta = document.createElement('div');
meta.className = 'message-meta';
const time = document.createElement('span');
time.textContent = item.timestamp || '';
const idChip = document.createElement('span');
idChip.textContent = `id=${item.id ?? ''}`;
meta.appendChild(time);
meta.appendChild(idChip);
bubble.appendChild(meta);
container.appendChild(sender);
container.appendChild(bubble);
if (!item.is_human && item.product_ids && item.product_ids.length) {
const products = document.createElement('div');
products.className = 'message bot';
products.style.marginTop = '6px';
products.textContent = `product_ids: ${JSON.stringify(item.product_ids, null, 2)}`;
container.appendChild(products);
messagesArea.insertBefore(createMessageEl(item, item), messagesArea.firstChild);
} else {
messagesArea.appendChild(createMessageEl(item, item));
}
const raw = document.createElement('div');
raw.className = 'raw-json';
raw.textContent = JSON.stringify(item, null, 2);
container.appendChild(raw);
return container;
totalMessages++;
});
nextCursor = payload.next_cursor;
loadMoreBtn.classList.toggle('visible', !!nextCursor);
// Update stats
document.getElementById('statMessages').textContent = totalMessages;
document.getElementById('statProducts').textContent = totalProducts;
const dateRange = dateFrom && dateTo ? `${dateFrom}${dateTo}` : (dateFrom || dateTo || 'Tất cả');
document.getElementById('statDateRange').textContent = dateRange;
document.getElementById('statsBar').style.display = '';
// Scroll to bottom on first load
if (!beforeId) {
const scroll = document.getElementById('chatScroll');
scroll.scrollTop = scroll.scrollHeight;
}
</script>
} catch (err) {
alert('Lỗi: ' + err.message);
}
}
</script>
</body>
</html>
......@@ -5,9 +5,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa Chatbot Test</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
......
......@@ -4,7 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Realtime Monitor</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<style>
* { box-sizing: border-box; }
......
......@@ -4,8 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa AI Platform</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/dashboard.css?v=4">
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/dashboard.css?v=4">
<style>
/* ═══ MAIN LAYOUT OVERRIDES ═══ */
body{margin:0;display:flex;min-height:100vh}
......@@ -49,22 +51,22 @@ body{margin:0;display:flex;min-height:100vh}
.sidebar-scroll::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 4px; }
.sidebar-scroll:hover::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); }
/* Settings Modal */
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
.modal-overlay.open { display: flex; animation: fadeIn 0.2s; }
.modal-content { background: white; border-radius: 12px; padding: 24px; width: 420px; max-width: 90%; box-shadow: 0 10px 25px rgba(0,0,0,0.1); }
.modal-header h3 { margin: 0; font-size: 16px; font-weight: 700; color:var(--text-main); }
.modal-body { margin: 20px 0; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 12px; font-weight: 600; color:var(--text-muted); margin-bottom: 6px; }
.form-group input { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 13px; color:var(--text-main); }
.form-group input:focus { outline:none; border-color:var(--gold); }
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; }
.btn { padding: 9px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition:all 0.2s; }
.btn-cancel { background: #f1f5f9; color: #475569; }
.btn-cancel:hover { background: #e2e8f0; }
.btn-save { background: var(--gold); color: white; }
.btn-save:hover { background: var(--gold-d,#B39356); }
/* Settings Modal (Memos-style) */
.settings-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.25); z-index:1000; display:none; justify-content:center; align-items:center; backdrop-filter:blur(2px); }
.settings-overlay.open { display:flex; }
.settings-dialog { background:var(--card,#fff); border:1px solid var(--border,#e7e5e2); border-radius:12px; box-shadow:var(--shadow-lg, 0 4px 12px rgba(0,0,0,0.08)); width:680px; max-width:92vw; height:520px; max-height:85vh; display:flex; flex-direction:column; overflow:hidden; }
.settings-body { display:flex; flex:1; min-height:0; }
.settings-sidebar { width:180px; border-right:1px solid var(--border,#e7e5e2); display:flex; flex-direction:column; padding:16px 12px; gap:2px; background:var(--card,#fff); }
.settings-sidebar .group-label { font-size:11px; font-weight:600; color:var(--muted-fg,#78716c); padding:4px 12px; text-transform:uppercase; letter-spacing:0.05em; }
.settings-sidebar .group-label.admin { margin-top:16px; }
.settings-content { flex:1; display:flex; flex-direction:column; min-width:0; }
.settings-scroll { flex:1; overflow-y:auto; padding:20px 24px; }
.settings-footer { padding:12px 24px; border-top:1px solid var(--border,#e7e5e2); display:flex; justify-content:flex-end; gap:8px; background:var(--card,#fff); }
.settings-user-block { border-top:1px solid var(--border,#e7e5e2); padding-top:12px; margin-top:auto; }
.settings-user-row { display:flex; align-items:center; gap:8px; padding:4px 8px; }
.settings-user-avatar { width:28px; height:28px; border-radius:50%; background:var(--muted,#f0efec); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:600; color:var(--muted-fg,#78716c); flex-shrink:0; }
.settings-version { display:flex; align-items:center; gap:6px; padding:6px 8px 0; font-size:11px; color:var(--muted-fg,#78716c); }
.settings-version-dot { width:6px; height:6px; border-radius:50%; background:var(--success,#16a34a); flex-shrink:0; }
</style>
</head>
<body>
......@@ -81,6 +83,10 @@ body{margin:0;display:flex;min-height:100vh}
<div class="sidebar-scroll">
<div class="nav-group">
<div class="nav-group-label">Main</div>
<a data-page="roadmap.html" class="nav-item" onclick="navigateTo(this)">
<span class="nav-icon" style="font-size:10px;font-weight:800;letter-spacing:.04em">RM</span>
<span>Kế hoạch phát triển</span>
</a>
<a data-page="flow.html" class="nav-item" onclick="navigateTo(this)">
<span class="nav-icon"></span>
<span>Sơ đồ hoạt động</span>
......@@ -209,39 +215,232 @@ body{margin:0;display:flex;min-height:100vh}
</div>
<div class="sidebar-footer">
<div class="user-info" id="sidebarUserInfo" style="display:flex;align-items:center;gap:8px;padding:8px 12px;margin-bottom:8px;border-radius:8px;background:var(--bg)">
<div style="width:28px;height:28px;border-radius:50%;background:var(--gold-l,#FFFBEB);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;color:var(--gold,#B45309)" id="userAvatar">A</div>
<span style="flex:1;font-size:12px;color:var(--t);font-weight:600" id="userName">admin</span>
<button onclick="openSettingsModal()" title="Cài đặt LLM" style="background:none;border:none;cursor:pointer;color:var(--m);font-size:14px;padding:4px;transition:color .2s" onmouseover="this.style.color='var(--gold)'" onmouseout="this.style.color='var(--m)'"></button>
<button onclick="handleLogout()" title="Đăng xuất" style="background:none;border:none;cursor:pointer;color:var(--m);font-size:14px;padding:4px;transition:color .2s" onmouseover="this.style.color='#ef4444'" onmouseout="this.style.color='var(--m)'"></button>
</div>
<div class="version-info">
<span class="version-dot"></span>
<span class="version-text"><strong>v2.5.0</strong> · Online</span>
<div class="settings-user-block">
<div class="settings-user-row">
<div class="settings-user-avatar" id="userAvatar">A</div>
<span style="flex:1; font-size:13px; font-weight:500; color:var(--foreground,#1c1917);" id="userName">admin</span>
<button class="btn-icon" style="padding:4px;font-size:13px;" onclick="openSettingsModal()" title="Settings"></button>
<button class="btn-icon" style="padding:4px;font-size:13px;" onclick="handleLogout()" title="Đăng xuất"></button>
</div>
<div class="settings-version">
<span class="settings-version-dot"></span>
<strong>v2.5.0</strong> · Online
</div>
</div>
</div>
</aside>
<!-- ═══ LLM SETTINGS MODAL ═══ -->
<div id="llmSettingsModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h3>Cài đặt LLM Cá nhân</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label>Codex Auth Token</label>
<input type="password" id="inputCodexToken" placeholder="eyJhbGciOiJIUzI1NiIs..." />
<!-- ═══ SETTINGS MODAL (Memos-style: sidebar + content) ═══ -->
<div id="llmSettingsModal" class="settings-overlay" onclick="if(event.target===this)closeSettingsModal()">
<div class="settings-dialog">
<div class="settings-body">
<!-- LEFT SIDEBAR -->
<div class="settings-sidebar">
<span class="group-label">Basic</span>
<div class="section-menu-item active" onclick="switchMainTab('account')" data-tab="account">
<i class="fas fa-user icon" style="font-size:12px;width:16px;text-align:center;"></i> <span>My Account</span>
</div>
<div class="section-menu-item" onclick="switchMainTab('llm')" data-tab="llm">
<i class="fas fa-key icon" style="font-size:12px;width:16px;text-align:center;"></i> <span>LLM Keys</span>
</div>
<div class="section-menu-item" onclick="switchMainTab('connection')" data-tab="connection">
<i class="fas fa-database icon" style="font-size:12px;width:16px;text-align:center;"></i> <span>Kết nối</span>
</div>
<div class="section-menu-item" onclick="switchMainTab('display')" data-tab="display">
<i class="fas fa-palette icon" style="font-size:12px;width:16px;text-align:center;"></i> <span>Giao diện</span>
</div>
<span class="group-label admin">Admin</span>
<div class="section-menu-item" onclick="switchMainTab('advanced')" data-tab="advanced">
<i class="fas fa-cog icon" style="font-size:12px;width:16px;text-align:center;"></i> <span>Nâng cao</span>
</div>
<!-- Spacer -->
<div style="flex:1;"></div>
<!-- User row at bottom of settings sidebar -->
<div class="settings-user-block">
<div class="settings-user-row">
<div class="settings-user-avatar" id="settingsAvatar">A</div>
<span style="flex:1; font-size:13px; font-weight:500; color:var(--foreground,#1c1917);" id="settingsUserName">admin</span>
<button class="btn-icon" style="padding:4px;" title="Copy ID" onclick="navigator.clipboard.writeText(localStorage.getItem('canifa_user')||'')"><i style="font-size:11px;" class="fas fa-copy"></i></button>
</div>
<div class="settings-version">
<span class="settings-version-dot"></span>
<strong>v2.5.0</strong> · Online
</div>
</div>
</div>
<div class="form-group">
<label>OpenAI API Key (Tuỳ chọn)</label>
<input type="password" id="inputOpenAiKey" placeholder="sk-..." />
<!-- RIGHT CONTENT -->
<div class="settings-content">
<div class="settings-scroll">
<!-- TAB: Account -->
<div id="mainTab_account" class="setting-tab-content">
<div class="setting-section">
<div class="setting-section-header">
<h3>My Account</h3>
<p class="desc">Thông tin tài khoản cá nhân</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Username</span>
</div>
<div class="setting-control">
<span id="settingUsername" style="font-size:13px; font-weight:600; color:var(--foreground);"></span>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Role</span>
</div>
<div class="setting-control">
<span class="badge badge-info" id="settingRole">user</span>
</div>
</div>
</div>
</div>
</div>
<!-- TAB: LLM Keys -->
<div id="mainTab_llm" class="setting-tab-content" style="display:none;">
<div class="setting-section">
<div class="setting-section-header">
<h3>LLM API Keys</h3>
<p class="desc">Cấu hình API keys cho các mô hình AI</p>
</div>
<div class="setting-section-body">
<div class="setting-row vertical">
<div class="setting-label">
<span class="setting-label-text">Codex Auth Token</span>
<p class="setting-label-desc">Token xác thực cho Codex API</p>
</div>
<div class="setting-control" style="width:100%;">
<input class="input" type="password" id="inputCodexToken" placeholder="eyJhbGciOiJIUzI1NiIs..." style="width:100%; font-family:var(--font-mono,monospace);">
</div>
</div>
<div class="setting-row vertical">
<div class="setting-label">
<span class="setting-label-text">OpenAI API Key</span>
<p class="setting-label-desc">Tuỳ chọn — dùng cho GPT-4o, GPT-5.4</p>
</div>
<div class="setting-control" style="width:100%;">
<input class="input" type="password" id="inputOpenAiKey" placeholder="sk-..." style="width:100%; font-family:var(--font-mono,monospace);">
</div>
</div>
</div>
</div>
</div>
<!-- TAB: Connection -->
<div id="mainTab_connection" class="setting-tab-content" style="display:none;">
<div class="setting-section">
<div class="setting-section-header">
<h3>Kết nối</h3>
<p class="desc">Cấu hình endpoint và database</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Backend API</span>
<p class="setting-label-desc">URL backend server</p>
</div>
<div class="setting-control">
<span style="font-size:12px; font-family:var(--font-mono,monospace); color:var(--muted-fg);" id="settingBackendUrl"></span>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Trạng thái</span>
<p class="setting-label-desc">Kết nối tới backend</p>
</div>
<div class="setting-control">
<span class="badge badge-success" id="settingConnStatus">Connected</span>
</div>
</div>
<hr class="separator">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Langfuse</span>
<p class="setting-label-desc">Observability & tracing</p>
</div>
<div class="setting-control">
<a href="http://172.16.2.210:3100" target="_blank" class="btn btn-outline btn-sm"><i class="fas fa-external-link-alt" style="margin-right:4px;"></i> Open</a>
</div>
</div>
</div>
</div>
</div>
<!-- TAB: Display -->
<div id="mainTab_display" class="setting-tab-content" style="display:none;">
<div class="setting-section">
<div class="setting-section-header">
<h3>Giao diện</h3>
<p class="desc">Tùy chỉnh theme và hiển thị</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Theme</span>
<p class="setting-label-desc">Giao diện sáng/tối</p>
</div>
<div class="setting-control">
<select class="select" disabled><option>Light (Memos)</option></select>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Sidebar mặc định</span>
<p class="setting-label-desc">Mở rộng hoặc thu gọn</p>
</div>
<div class="setting-control">
<select class="select"><option>Expanded</option><option>Collapsed</option></select>
</div>
</div>
</div>
</div>
</div>
<!-- TAB: Advanced -->
<div id="mainTab_advanced" class="setting-tab-content" style="display:none;">
<div class="setting-section">
<div class="setting-section-header">
<h3>Nâng cao</h3>
<p class="desc">Cấu hình admin</p>
</div>
<div class="setting-section-body">
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Clear Cache</span>
<p class="setting-label-desc">Xóa cache sản phẩm & embedding</p>
</div>
<div class="setting-control">
<button class="btn btn-outline btn-sm" onclick="fetch('/api/cache/clear',{method:'POST',headers:{'Authorization':'Bearer '+localStorage.getItem('canifa_token')}}).then(()=>alert('Cache cleared!'))"><i class="fas fa-broom" style="margin-right:4px;"></i> Clear</button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">
<span class="setting-label-text">Health Check</span>
<p class="setting-label-desc">Kiểm tra trạng thái hệ thống</p>
</div>
<div class="setting-control">
<button class="btn btn-outline btn-sm" onclick="fetch('/health').then(r=>r.json()).then(d=>alert(JSON.stringify(d,null,2)))"><i class="fas fa-heartbeat" style="margin-right:4px;"></i> Check</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="settings-footer">
<button class="btn btn-outline" onclick="closeSettingsModal()">Hủy</button>
<button class="btn btn-primary" onclick="saveSettingsModal()"><i class="fas fa-check" style="margin-right:4px;"></i> Lưu</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-cancel" onclick="closeSettingsModal()">Hủy</button>
<button class="btn btn-save" onclick="saveSettingsModal()">Lưu lại</button>
</div>
</div>
</div>
......@@ -292,6 +491,15 @@ function handleLogout() {
}
// ═══ SETTINGS MODAL LOGIC ═══
function switchMainTab(tabName) {
document.querySelectorAll('#llmSettingsModal .setting-tab-content').forEach(el => el.style.display = 'none');
const tab = document.getElementById('mainTab_' + tabName);
if (tab) tab.style.display = '';
document.querySelectorAll('#llmSettingsModal .section-menu-item').forEach(el => {
el.classList.toggle('active', el.dataset.tab === tabName);
});
}
async function loadUserSettings() {
const token = localStorage.getItem('canifa_token');
if (!token) return;
......@@ -303,15 +511,27 @@ async function loadUserSettings() {
const settings = user.settings || {};
document.getElementById('inputCodexToken').value = settings.codex_token || '';
document.getElementById('inputOpenAiKey').value = settings.openai_key || '';
// update cached user settings just in case
localStorage.setItem('canifa_user', JSON.stringify(user));
}
} catch(e) { console.error('Failed to load settings', e); }
}
function openSettingsModal() {
document.getElementById('llmSettingsModal').classList.add('open');
loadUserSettings(); // refresh on open
const modal = document.getElementById('llmSettingsModal');
// Populate user info
try {
const user = JSON.parse(localStorage.getItem('canifa_user') || '{}');
const name = user.username || 'user';
document.getElementById('settingsUserName').textContent = name;
document.getElementById('settingsAvatar').textContent = name.charAt(0).toUpperCase();
document.getElementById('settingUsername').textContent = name;
document.getElementById('settingRole').textContent = user.role || 'user';
document.getElementById('settingBackendUrl').textContent = window.location.origin;
} catch(e) {}
// Reset to first tab
switchMainTab('account');
modal.classList.add('open');
loadUserSettings();
}
function closeSettingsModal() {
......@@ -319,9 +539,9 @@ function closeSettingsModal() {
}
async function saveSettingsModal() {
const btn = document.querySelector('.btn-save');
const ogText = btn.textContent;
btn.textContent = 'Đang lưu...';
const btn = document.querySelector('.settings-footer .btn-primary');
const ogHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right:4px;"></i> Đang lưu...';
btn.disabled = true;
const codex = document.getElementById('inputCodexToken').value.trim();
......@@ -346,7 +566,7 @@ async function saveSettingsModal() {
} catch(e) {
alert('Lỗi hệ thống');
} finally {
btn.textContent = ogText;
btn.innerHTML = ogHTML;
btn.disabled = false;
}
}
......
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Team Notes - Canifa AI System</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: var(--bg, #FAF6F0); color: var(--t, #2C1810); min-height: 100vh; display: flex; }
/* ═══ SIDEBAR ═══ */
/* --- SIDEBAR --- */
.sidebar { width: 260px; min-height: 100vh; background: var(--s, #FFFFFF); border-right: 1px solid var(--b, #E8DED0); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; z-index: 100; }
.sidebar-brand { padding: 24px 20px; border-bottom: 1px solid var(--b, #E8DED0); display: flex; align-items: center; gap: 12px; }
.brand-icon { width: 40px; height: 40px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.3em; flex-shrink: 0; }
......@@ -35,26 +37,26 @@
.version-dot { width: 8px; height: 8px; border-radius: 50%; background: #56d364; box-shadow: 0 0 8px rgba(86,211,100,0.4); }
.version-text { font-size: 0.75em; color: var(--m, #6B5B4F); }
/* ═══ MAIN ═══ */
/* --- MAIN --- */
.main { margin-left: 260px; flex: 1; min-height: 100vh; display: flex; flex-direction: column; }
.topbar { padding: 24px 32px; border-bottom: 1px solid var(--b, #E8DED0); display: flex; justify-content: space-between; align-items: center; }
.topbar h1 { font-size: 1.5em; font-weight: 800; color: var(--t, #2C1810); }
.topbar p { font-size: 0.85em; color: #484f58; margin-top: 2px; }
.content { padding: 24px 32px; flex: 1; }
/* ═══ BUTTONS ═══ */
/* --- BUTTONS --- */
.btn { padding: 8px 16px; border-radius: 8px; font-size: 0.82em; font-weight: 600; border: 1px solid var(--b, #E8DED0); background: var(--bg, #FAF6F0); color: var(--t, #2C1810); cursor: pointer; transition: all 0.2s; }
.btn:hover { background: var(--b, #E8DED0); border-color: var(--b, #E8DED0); }
.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2); border: none; color: #fff; }
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
/* ═══ FILTERS ═══ */
/* --- FILTERS --- */
.filter-bar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
.filter-chip { padding: 6px 14px; border-radius: 20px; font-size: 0.78em; font-weight: 600; border: 1px solid var(--b, #E8DED0); background: transparent; color: var(--m, #6B5B4F); cursor: pointer; transition: all 0.2s; }
.filter-chip:hover { background: var(--bg, #FAF6F0); color: var(--t, #2C1810); }
.filter-chip.active { background: rgba(102,126,234,0.15); color: #a78bfa; border-color: rgba(102,126,234,0.3); }
/* ═══ NOTES ═══ */
/* --- NOTES --- */
.notes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 14px; }
.note-card { background: var(--s, #FFFFFF); border: 1px solid var(--b, #E8DED0); border-radius: 12px; padding: 18px; transition: all 0.25s; border-top: 3px solid var(--nc, var(--gold, #B8860B)); }
.note-card:hover { border-color: var(--b, #E8DED0); transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.3); }
......@@ -70,7 +72,7 @@
.note-act-btn:hover { background: var(--bg, #FAF6F0); color: var(--t, #2C1810); border-color: var(--b, #E8DED0); }
.note-act-btn.pin-active { color: #e3b341; border-color: rgba(227,179,65,0.3); }
/* ═══ MODAL ═══ */
/* --- MODAL --- */
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 200; align-items: center; justify-content: center; }
.modal-overlay.show { display: flex; }
.modal { background: var(--bg, #FAF6F0); border: 1px solid var(--b, #E8DED0); border-radius: 16px; width: 500px; max-width: 90vw; max-height: 85vh; overflow-y: auto; }
......@@ -98,7 +100,7 @@
.header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.header-row h2 { font-size: 1.2em; font-weight: 700; color: var(--t, #2C1810); }
/* ═══ USAGE GUIDE ═══ */
/* --- USAGE GUIDE --- */
.guide { margin-top: 40px; border-top: 1px solid var(--b, #E8DED0); padding-top: 20px; }
.guide-toggle { display: flex; align-items: center; gap: 8px; cursor: pointer; color: #484f58; font-size: 0.82em; font-weight: 600; background: none; border: none; padding: 6px 0; transition: color 0.2s; }
.guide-toggle:hover { color: var(--m, #6B5B4F); }
......@@ -124,59 +126,59 @@
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">🤖</div>
<div class="brand-icon">??</div>
<div class="brand-text"><h2>Canifa AI</h2><span>Admin Console</span></div>
</div>
<div class="nav-group">
<div class="nav-group-label">Main</div>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">🔀</span><span>Sơ đồ hoạt động</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">💬</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">🧾</span><span>History</span></a>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">??</span><span>So d? ho?t d?ng</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">??</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">??</span><span>History</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Workspace</div>
<a href="/static/resources.html" class="nav-item"><span class="nav-icon">🔗</span><span>Resources</span></a>
<a href="/static/notes.html" class="nav-item active"><span class="nav-icon">📝</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item"><span class="nav-icon">📋</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item"><span class="nav-icon">📖</span><span>Hướng dẫn</span></a>
<a href="/static/resources.html" class="nav-item"><span class="nav-icon">??</span><span>Resources</span></a>
<a href="/static/notes.html" class="nav-item active"><span class="nav-icon">??</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item"><span class="nav-icon">??</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item"><span class="nav-icon">??</span><span>Hu?ng d?n</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Thử nghiệm</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">🗄️</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">🔍</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">📝</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
<div class="nav-group-label">Th? nghi?m</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">???</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">??</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">??</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">External</div>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">📚</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">📖</span><span>ReDoc</span></a>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>ReDoc</span></a>
</div>
<div class="sidebar-footer">
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> · Online</span></div>
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> Online</span></div>
</div>
</aside>
<div class="main">
<div class="topbar">
<div><h1>📝 Team Notes</h1><p>Notes, pinned docs & team workspace</p></div>
<div><h1>?? Team Notes</h1><p>Notes, pinned docs & team workspace</p></div>
</div>
<div class="content">
<div class="header-row">
<h2>📝 Team Notes & Pinned Docs</h2>
<h2>?? Team Notes & Pinned Docs</h2>
<button class="btn btn-primary" onclick="openModal()">+ New Note</button>
</div>
<div class="filter-bar">
<button class="filter-chip active" onclick="filter('all', this)">All</button>
<button class="filter-chip" onclick="filter('pinned', this)">📌 Pinned</button>
<button class="filter-chip" onclick="filter('note', this)">📝 Notes</button>
<button class="filter-chip" onclick="filter('doc', this)">📄 Docs</button>
<button class="filter-chip" onclick="filter('todo', this)"> To-do</button>
<button class="filter-chip" onclick="filter('announcement', this)">📢 Announcements</button>
<button class="filter-chip" onclick="filter('pinned', this)">?? Pinned</button>
<button class="filter-chip" onclick="filter('note', this)">?? Notes</button>
<button class="filter-chip" onclick="filter('doc', this)">?? Docs</button>
<button class="filter-chip" onclick="filter('todo', this)">? To-do</button>
<button class="filter-chip" onclick="filter('announcement', this)">?? Announcements</button>
</div>
<div class="notes-grid" id="notesGrid">
<div class="empty-state"><div class="empty-icon">📝</div><p>No notes yet. Create your first one!</p></div>
<div class="empty-state"><div class="empty-icon">??</div><p>No notes yet. Create your first one!</p></div>
</div>
......@@ -186,15 +188,15 @@
<!-- MODAL -->
<div class="modal-overlay" id="modal">
<div class="modal">
<div class="modal-head"><h3 id="modalTitle">📝 New Note</h3><button class="modal-close" onclick="closeModal()">×</button></div>
<div class="modal-head"><h3 id="modalTitle">?? New Note</h3><button class="modal-close" onclick="closeModal()"></button></div>
<div class="modal-body">
<input type="hidden" id="editId">
<div class="form-group"><label class="form-label">Title</label><input type="text" class="form-input" id="fTitle" placeholder="Note title..."></div>
<div class="form-group"><label class="form-label">Content</label><textarea class="form-textarea" id="fContent" placeholder="Write your note..."></textarea></div>
<div class="form-group"><label class="form-label">Category</label>
<select class="form-select" id="fCat">
<option value="note">📝 Note</option><option value="doc">📄 Document</option>
<option value="todo">✅ To-do</option><option value="announcement">📢 Announcement</option>
<option value="note">?? Note</option><option value="doc">?? Document</option>
<option value="todo">? To-do</option><option value="announcement">?? Announcement</option>
</select>
</div>
<div class="form-group"><label class="form-label">Color</label>
......@@ -207,17 +209,17 @@
<div class="color-dot" style="background:#f778ba" onclick="pickColor('#f778ba',this)"></div>
</div>
</div>
<div class="form-group"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="fPin"> <span class="form-label" style="margin:0">📌 Pin this note</span></label></div>
<div class="form-group"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="fPin"> <span class="form-label" style="margin:0">?? Pin this note</span></label></div>
</div>
<div class="modal-foot"><button class="btn" onclick="closeModal()">Cancel</button><button class="btn btn-primary" onclick="save()">💾 Save Note</button></div>
<div class="modal-foot"><button class="btn" onclick="closeModal()">Cancel</button><button class="btn btn-primary" onclick="save()">?? Save Note</button></div>
</div>
</div>
<script>
let data = [], activeFilter = 'all', selectedColor = 'var(--gold, #B8860B)';
function esc(s) { return s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : ''; }
function fmtTime(iso) { if(!iso)return''; const d=new Date(iso),now=new Date(),diff=(now-d)/1000; if(diff<60)return'vừa xong'; if(diff<3600)return Math.floor(diff/60)+' phút trước'; if(diff<86400)return Math.floor(diff/3600)+' giờ trước'; if(diff<604800)return Math.floor(diff/86400)+' ngày trước'; return d.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit',year:'numeric'}); }
const catIcons = {note:'📝',doc:'📄',todo:'✅',announcement:'📢'};
function fmtTime(iso) { if(!iso)return''; const d=new Date(iso),now=new Date(),diff=(now-d)/1000; if(diff<60)return'v?a xong'; if(diff<3600)return Math.floor(diff/60)+' pht tru?c'; if(diff<86400)return Math.floor(diff/3600)+' gi? tru?c'; if(diff<604800)return Math.floor(diff/86400)+' ngy tru?c'; return d.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit',year:'numeric'}); }
const catIcons = {note:'??',doc:'??',todo:'?',announcement:'??'};
async function load() {
try { const r=await fetch('/api/dashboard/notes'); const j=await r.json(); data=j.notes||[]; render(); }
......@@ -231,22 +233,22 @@
else if (activeFilter !== 'all') f = data.filter(n=>n.category===activeFilter);
f.sort((a,b)=>(a.pinned&&!b.pinned?-1:!a.pinned&&b.pinned?1:new Date(b.updated_at||b.created_at)-new Date(a.updated_at||a.created_at)));
if (!f.length) { grid.innerHTML = `<div class="empty-state"><div class="empty-icon">📝</div><p>No notes found.</p></div>`; return; }
if (!f.length) { grid.innerHTML = `<div class="empty-state"><div class="empty-icon">??</div><p>No notes found.</p></div>`; return; }
grid.innerHTML = f.map(n=>`
<div class="note-card ${n.pinned?'pinned':''}" style="--nc:${n.color||'var(--gold, #B8860B)'}">
<div class="note-header">
<span class="note-cat">${catIcons[n.category]||'📝'} ${n.category}</span>
${n.pinned?'<span style="font-size:0.8em">📌</span>':''}
<span class="note-cat">${catIcons[n.category]||'??'} ${n.category}</span>
${n.pinned?'<span style="font-size:0.8em">??</span>':''}
</div>
<div class="note-title">${esc(n.title)}</div>
<div class="note-body">${esc(n.content)}</div>
<div class="note-footer">
<span class="note-time">${fmtTime(n.updated_at||n.created_at)}</span>
<div class="note-actions">
<button class="note-act-btn ${n.pinned?'pin-active':''}" title="Pin" onclick="togglePin('${n.id}')">📌</button>
<button class="note-act-btn" title="Edit" onclick="edit('${n.id}')">✏️</button>
<button class="note-act-btn" title="Delete" onclick="del('${n.id}')">🗑️</button>
<button class="note-act-btn ${n.pinned?'pin-active':''}" title="Pin" onclick="togglePin('${n.id}')">??</button>
<button class="note-act-btn" title="Edit" onclick="edit('${n.id}')">??</button>
<button class="note-act-btn" title="Delete" onclick="del('${n.id}')">???</button>
</div>
</div>
</div>`).join('');
......@@ -255,10 +257,10 @@
function filter(f,el) { activeFilter=f; document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active')); el.classList.add('active'); render(); }
function pickColor(c,el) { selectedColor=c; document.querySelectorAll('.color-dot').forEach(d=>d.classList.remove('active')); el.classList.add('active'); }
function openModal() { document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=''; document.getElementById('fTitle').value=''; document.getElementById('fContent').value=''; document.getElementById('fCat').value='note'; document.getElementById('fPin').checked=false; selectedColor='var(--gold, #B8860B)'; document.querySelectorAll('.color-dot').forEach(d=>{d.classList.remove('active');if(d.style.background==='rgb(88, 166, 255)')d.classList.add('active');}); document.getElementById('modalTitle').textContent='📝 New Note'; setTimeout(()=>document.getElementById('fTitle').focus(),100); }
function openModal() { document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=''; document.getElementById('fTitle').value=''; document.getElementById('fContent').value=''; document.getElementById('fCat').value='note'; document.getElementById('fPin').checked=false; selectedColor='var(--gold, #B8860B)'; document.querySelectorAll('.color-dot').forEach(d=>{d.classList.remove('active');if(d.style.background==='rgb(88, 166, 255)')d.classList.add('active');}); document.getElementById('modalTitle').textContent='?? New Note'; setTimeout(()=>document.getElementById('fTitle').focus(),100); }
function closeModal() { document.getElementById('modal').classList.remove('show'); }
function edit(id) { const n=data.find(x=>x.id===id); if(!n)return; document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=id; document.getElementById('fTitle').value=n.title; document.getElementById('fContent').value=n.content||''; document.getElementById('fCat').value=n.category; document.getElementById('fPin').checked=n.pinned; selectedColor=n.color||'var(--gold, #B8860B)'; document.querySelectorAll('.color-dot').forEach(d=>{d.classList.remove('active');if(d.style.background===selectedColor||`rgb(${parseInt(selectedColor.slice(1,3),16)}, ${parseInt(selectedColor.slice(3,5),16)}, ${parseInt(selectedColor.slice(5,7),16)})`===d.style.background)d.classList.add('active');}); document.getElementById('modalTitle').textContent='✏️ Edit Note'; }
function edit(id) { const n=data.find(x=>x.id===id); if(!n)return; document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=id; document.getElementById('fTitle').value=n.title; document.getElementById('fContent').value=n.content||''; document.getElementById('fCat').value=n.category; document.getElementById('fPin').checked=n.pinned; selectedColor=n.color||'var(--gold, #B8860B)'; document.querySelectorAll('.color-dot').forEach(d=>{d.classList.remove('active');if(d.style.background===selectedColor||`rgb(${parseInt(selectedColor.slice(1,3),16)}, ${parseInt(selectedColor.slice(3,5),16)}, ${parseInt(selectedColor.slice(5,7),16)})`===d.style.background)d.classList.add('active');}); document.getElementById('modalTitle').textContent='?? Edit Note'; }
async function save() {
const id=document.getElementById('editId').value,title=document.getElementById('fTitle').value.trim(),content=document.getElementById('fContent').value.trim();
......
......@@ -4,9 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hướng dẫn sử dụng — Product Performance</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/product.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/product.css">
<script src="/static/js/frame-detect.js"></script>
<style>
.guide-content { max-width: 900px; padding: 32px 40px; }
.guide-content h1 { font-size: 1.6em; color: var(--gold); margin-bottom: 8px; font-family: 'Fraunces', serif; }
......
......@@ -4,11 +4,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Performance — Canifa AI</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/product.css">
<script src="/static/frame-detect.js"></script>
<script src="/static/stock-loader.js"></script>
<script src="/static/product-render.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/product.css">
<script src="/static/js/frame-detect.js"></script>
<script src="/static/js/stock-loader.js"></script>
<script src="/static/js/product-render.js"></script>
</head>
<body>
......
......@@ -4,7 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prompt Optimizer</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{min-height:100vh;color:#18181B;background:radial-gradient(circle at top right,rgba(180,83,9,.10),transparent 28%),linear-gradient(180deg,#F7F4EE 0%,#F3EFE6 100%);font-family:'Outfit',sans-serif}
......
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Resources - Canifa AI System</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: var(--bg, #FAF6F0); color: var(--t, #2C1810); min-height: 100vh; display: flex; }
/* ═══ SIDEBAR ═══ */
/* --- SIDEBAR --- */
.sidebar { width: 260px; min-height: 100vh; background: var(--s, #FFFFFF); border-right: 1px solid var(--b, #E8DED0); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; z-index: 100; }
.sidebar-brand { padding: 24px 20px; border-bottom: 1px solid var(--b, #E8DED0); display: flex; align-items: center; gap: 12px; }
.brand-icon { width: 40px; height: 40px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.3em; flex-shrink: 0; }
......@@ -35,26 +37,26 @@
.version-dot { width: 8px; height: 8px; border-radius: 50%; background: #56d364; box-shadow: 0 0 8px rgba(86,211,100,0.4); }
.version-text { font-size: 0.75em; color: var(--m, #6B5B4F); }
/* ═══ MAIN ═══ */
/* --- MAIN --- */
.main { margin-left: 260px; flex: 1; min-height: 100vh; display: flex; flex-direction: column; }
.topbar { padding: 24px 32px; border-bottom: 1px solid var(--b, #E8DED0); display: flex; justify-content: space-between; align-items: center; }
.topbar h1 { font-size: 1.5em; font-weight: 800; color: var(--t, #2C1810); }
.topbar p { font-size: 0.85em; color: #484f58; margin-top: 2px; }
.content { padding: 24px 32px; flex: 1; }
/* ═══ BUTTONS ═══ */
/* --- BUTTONS --- */
.btn { padding: 8px 16px; border-radius: 8px; font-size: 0.82em; font-weight: 600; border: 1px solid var(--b, #E8DED0); background: var(--bg, #FAF6F0); color: var(--t, #2C1810); cursor: pointer; transition: all 0.2s; }
.btn:hover { background: var(--b, #E8DED0); border-color: var(--b, #E8DED0); }
.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2); border: none; color: #fff; }
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
/* ═══ FILTERS ═══ */
/* --- FILTERS --- */
.filter-bar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
.filter-chip { padding: 6px 14px; border-radius: 20px; font-size: 0.78em; font-weight: 600; border: 1px solid var(--b, #E8DED0); background: transparent; color: var(--m, #6B5B4F); cursor: pointer; transition: all 0.2s; }
.filter-chip:hover { background: var(--bg, #FAF6F0); color: var(--t, #2C1810); }
.filter-chip.active { background: rgba(102,126,234,0.15); color: #a78bfa; border-color: rgba(102,126,234,0.3); }
/* ═══ LINKS ═══ */
/* --- LINKS --- */
.links-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 14px; }
.link-card { background: var(--s, #FFFFFF); border: 1px solid var(--b, #E8DED0); border-radius: 12px; padding: 16px; transition: all 0.25s; display: flex; gap: 14px; align-items: flex-start; }
.link-card:hover { border-color: var(--b, #E8DED0); transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.3); }
......@@ -76,7 +78,7 @@
.link-act { width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--b, #E8DED0); background: transparent; color: #484f58; font-size: 0.78em; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
.link-act:hover { background: var(--bg, #FAF6F0); color: var(--t, #2C1810); border-color: var(--b, #E8DED0); }
/* ═══ MODAL ═══ */
/* --- MODAL --- */
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 200; align-items: center; justify-content: center; }
.modal-overlay.show { display: flex; }
.modal { background: var(--bg, #FAF6F0); border: 1px solid var(--b, #E8DED0); border-radius: 16px; width: 500px; max-width: 90vw; max-height: 85vh; overflow-y: auto; }
......@@ -100,7 +102,7 @@
.header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.header-row h2 { font-size: 1.2em; font-weight: 700; color: var(--t, #2C1810); }
/* ═══ USAGE GUIDE ═══ */
/* --- USAGE GUIDE --- */
.guide { margin-top: 40px; border-top: 1px solid var(--b, #E8DED0); padding-top: 20px; }
.guide-toggle { display: flex; align-items: center; gap: 8px; cursor: pointer; color: #484f58; font-size: 0.82em; font-weight: 600; background: none; border: none; padding: 6px 0; transition: color 0.2s; }
.guide-toggle:hover { color: var(--m, #6B5B4F); }
......@@ -126,41 +128,41 @@
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">🤖</div>
<div class="brand-icon">??</div>
<div class="brand-text"><h2>Canifa AI</h2><span>Admin Console</span></div>
</div>
<div class="nav-group">
<div class="nav-group-label">Main</div>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">🔀</span><span>Sơ đồ hoạt động</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">💬</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">🧾</span><span>History</span></a>
<a href="/static/flow.html" class="nav-item"><span class="nav-icon">??</span><span>So d? ho?t d?ng</span></a>
<a href="/static/experiment_detail.html?id=exp_chatbot_prod" class="nav-item"><span class="nav-icon">??</span><span>Chatbot</span><span class="nav-badge badge-live">LIVE</span></a>
<a href="/static/history.html" class="nav-item"><span class="nav-icon">??</span><span>History</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Workspace</div>
<a href="/static/resources.html" class="nav-item active"><span class="nav-icon">🔗</span><span>Resources</span><span class="nav-badge badge-count" id="linksBadge">0</span></a>
<a href="/static/notes.html" class="nav-item"><span class="nav-icon">📝</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item"><span class="nav-icon">📋</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item"><span class="nav-icon">📖</span><span>Hướng dẫn</span></a>
<a href="/static/resources.html" class="nav-item active"><span class="nav-icon">??</span><span>Resources</span><span class="nav-badge badge-count" id="linksBadge">0</span></a>
<a href="/static/notes.html" class="nav-item"><span class="nav-icon">??</span><span>Team Notes</span></a>
<a href="/static/changelog.html" class="nav-item"><span class="nav-icon">??</span><span>Changelog</span></a>
<a href="/static/guide.html" class="nav-item"><span class="nav-icon">??</span><span>Hu?ng d?n</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">Thử nghiệm</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">🗄️</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">🔍</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">📝</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
<div class="nav-group-label">Th? nghi?m</div>
<a href="/static/test_sql.html" class="nav-item"><span class="nav-icon">???</span><span>Text-to-SQL</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/test_db.html" class="nav-item"><span class="nav-icon">??</span><span>DB Test</span><span class="nav-badge badge-beta">BETA</span></a>
<a href="/static/feedback_demo.html" class="nav-item"><span class="nav-icon">??</span><span>Feedback Demo</span><span class="nav-badge badge-new">NEW</span></a>
</div>
<div class="nav-group">
<div class="nav-group-label">External</div>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">📚</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">📖</span><span>ReDoc</span></a>
<a href="/docs" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>API Docs</span></a>
<a href="/redoc" target="_blank" class="nav-item"><span class="nav-icon">??</span><span>ReDoc</span></a>
</div>
<div class="sidebar-footer">
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> · Online</span></div>
<div class="version-info"><span class="version-dot"></span><span class="version-text"><strong>v2.5.0</strong> Online</span></div>
</div>
</aside>
<div class="main">
<div class="topbar">
<div><h1>🔗 Resources</h1><p>Documentation, links & team resources</p></div>
<div><h1>?? Resources</h1><p>Documentation, links & team resources</p></div>
</div>
<div class="content">
<div class="header-row">
......@@ -170,16 +172,16 @@
<div class="filter-bar" id="filterBar">
<button class="filter-chip active" onclick="filter('all', this)">All</button>
<button class="filter-chip" onclick="filter('pinned', this)">📌 Pinned</button>
<button class="filter-chip" onclick="filter('tool', this)">🔧 Tools</button>
<button class="filter-chip" onclick="filter('doc', this)">📄 Docs</button>
<button class="filter-chip" onclick="filter('api', this)">🔌 APIs</button>
<button class="filter-chip" onclick="filter('design', this)">🎨 Design</button>
<button class="filter-chip" onclick="filter('repo', this)">📦 Repos</button>
<button class="filter-chip" onclick="filter('pinned', this)">?? Pinned</button>
<button class="filter-chip" onclick="filter('tool', this)">?? Tools</button>
<button class="filter-chip" onclick="filter('doc', this)">?? Docs</button>
<button class="filter-chip" onclick="filter('api', this)">?? APIs</button>
<button class="filter-chip" onclick="filter('design', this)">?? Design</button>
<button class="filter-chip" onclick="filter('repo', this)">?? Repos</button>
</div>
<div class="links-grid" id="linksGrid">
<div class="empty-state"><div class="empty-icon">🔗</div><p>No resources yet. Add your first link!</p></div>
<div class="empty-state"><div class="empty-icon">??</div><p>No resources yet. Add your first link!</p></div>
</div>
......@@ -190,8 +192,8 @@
<div class="modal-overlay" id="modal">
<div class="modal">
<div class="modal-head">
<h3 id="modalTitle">🔗 Add Resource</h3>
<button class="modal-close" onclick="closeModal()">×</button>
<h3 id="modalTitle">?? Add Resource</h3>
<button class="modal-close" onclick="closeModal()"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editId">
......@@ -201,17 +203,17 @@
<div class="form-row">
<div class="form-group"><label class="form-label">Category</label>
<select class="form-select" id="fCat">
<option value="tool">🔧 Tool</option><option value="doc">📄 Document</option><option value="api">🔌 API</option>
<option value="design">🎨 Design</option><option value="repo">📦 Repository</option><option value="other">🔗 Other</option>
<option value="tool">?? Tool</option><option value="doc">?? Document</option><option value="api">?? API</option>
<option value="design">?? Design</option><option value="repo">?? Repository</option><option value="other">?? Other</option>
</select>
</div>
<div class="form-group"><label class="form-label">Icon (emoji)</label><input type="text" class="form-input" id="fIcon" placeholder="🔗" maxlength="4"></div>
<div class="form-group"><label class="form-label">Icon (emoji)</label><input type="text" class="form-input" id="fIcon" placeholder="??" maxlength="4"></div>
</div>
<div class="form-group"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="fPin"> <span class="form-label" style="margin:0">📌 Pin this resource</span></label></div>
<div class="form-group"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="fPin"> <span class="form-label" style="margin:0">?? Pin this resource</span></label></div>
</div>
<div class="modal-foot">
<button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="save()">💾 Save</button>
<button class="btn btn-primary" onclick="save()">?? Save</button>
</div>
</div>
</div>
......@@ -239,11 +241,11 @@
else if (activeFilter !== 'all') f = data.filter(l => l.category === activeFilter);
f.sort((a,b) => (a.pinned&&!b.pinned?-1:!a.pinned&&b.pinned?1:0));
if (!f.length) { grid.innerHTML = `<div class="empty-state"><div class="empty-icon">🔗</div><p>No resources found.</p></div>`; return; }
if (!f.length) { grid.innerHTML = `<div class="empty-state"><div class="empty-icon">??</div><p>No resources found.</p></div>`; return; }
grid.innerHTML = f.map(l => `
<div class="link-card ${l.pinned?'pinned':''}">
<div class="link-icon-box">${l.icon||'🔗'}</div>
<div class="link-icon-box">${l.icon||'??'}</div>
<div class="link-info">
<div class="link-cat-tag lcat-${l.category}">${l.category}</div>
<div class="link-title">${esc(l.title)}</div>
......@@ -251,19 +253,19 @@
${l.description?`<div class="link-desc">${esc(l.description)}</div>`:''}
</div>
<div class="link-actions-row">
<button class="link-act" title="Open" onclick="window.open('${l.url}','_blank')"></button>
<button class="link-act" title="Pin" onclick="togglePin('${l.id}')">📌</button>
<button class="link-act" title="Edit" onclick="edit('${l.id}')">✏️</button>
<button class="link-act" title="Delete" onclick="del('${l.id}')">🗑️</button>
<button class="link-act" title="Open" onclick="window.open('${l.url}','_blank')">?</button>
<button class="link-act" title="Pin" onclick="togglePin('${l.id}')">??</button>
<button class="link-act" title="Edit" onclick="edit('${l.id}')">??</button>
<button class="link-act" title="Delete" onclick="del('${l.id}')">???</button>
</div>
</div>`).join('');
}
function filter(f, el) { activeFilter=f; document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active')); el.classList.add('active'); render(); }
function openModal() { document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=''; document.getElementById('fTitle').value=''; document.getElementById('fUrl').value=''; document.getElementById('fDesc').value=''; document.getElementById('fCat').value='tool'; document.getElementById('fIcon').value=''; document.getElementById('fPin').checked=false; document.getElementById('modalTitle').textContent='🔗 Add Resource'; setTimeout(()=>document.getElementById('fTitle').focus(),100); }
function openModal() { document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=''; document.getElementById('fTitle').value=''; document.getElementById('fUrl').value=''; document.getElementById('fDesc').value=''; document.getElementById('fCat').value='tool'; document.getElementById('fIcon').value=''; document.getElementById('fPin').checked=false; document.getElementById('modalTitle').textContent='?? Add Resource'; setTimeout(()=>document.getElementById('fTitle').focus(),100); }
function closeModal() { document.getElementById('modal').classList.remove('show'); }
function edit(id) { const l=data.find(x=>x.id===id); if(!l)return; document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=id; document.getElementById('fTitle').value=l.title; document.getElementById('fUrl').value=l.url; document.getElementById('fDesc').value=l.description||''; document.getElementById('fCat').value=l.category; document.getElementById('fIcon').value=l.icon||''; document.getElementById('fPin').checked=l.pinned; document.getElementById('modalTitle').textContent='✏️ Edit Resource'; }
function edit(id) { const l=data.find(x=>x.id===id); if(!l)return; document.getElementById('modal').classList.add('show'); document.getElementById('editId').value=id; document.getElementById('fTitle').value=l.title; document.getElementById('fUrl').value=l.url; document.getElementById('fDesc').value=l.description||''; document.getElementById('fCat').value=l.category; document.getElementById('fIcon').value=l.icon||''; document.getElementById('fPin').checked=l.pinned; document.getElementById('modalTitle').textContent='?? Edit Resource'; }
async function save() {
const id=document.getElementById('editId').value, title=document.getElementById('fTitle').value.trim(), url=document.getElementById('fUrl').value.trim();
......
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kế hoạch phát triển — Canifa AI</title>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<script src="/static/js/frame-detect.js"></script>
<style>
body { margin:0; min-height:100vh; background:var(--bg); font-family:var(--font-sans); color:var(--foreground); }
.page { max-width:800px; margin:0 auto; padding:28px 24px 60px; }
.page-header { margin-bottom:32px; }
.page-header h1 { font-size:24px; font-weight:700; margin:0 0 6px; }
.page-header p { font-size:14px; color:var(--muted-fg); margin:0; }
.version-block { margin-bottom:32px; }
.version-title { display:flex; align-items:center; gap:10px; margin-bottom:14px; }
.version-title h2 { font-size:16px; font-weight:700; margin:0; }
.version-badge { font-size:11px; font-weight:700; padding:2px 8px; border-radius:999px; text-transform:uppercase; letter-spacing:.04em; }
.badge-current { background:var(--success-bg,#dcfce7); color:var(--success,#16a34a); }
.badge-next { background:var(--primary-light,#e0f2fe); color:var(--primary,#3b5998); }
.badge-future { background:var(--muted,#f0efec); color:var(--muted-fg,#78716c); }
.version-date { font-size:12px; color:var(--muted-fg); }
.feature-list { list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:8px; }
.feature-item { display:flex; align-items:flex-start; gap:10px; padding:10px 14px; background:var(--card); border:1px solid var(--border); border-radius:10px; }
.feature-check { width:18px; height:18px; border-radius:4px; border:2px solid var(--border); display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:1px; font-size:11px; }
.feature-check.done { background:var(--success,#16a34a); border-color:var(--success,#16a34a); color:white; }
.feature-check.wip { background:var(--warning-bg,#fef3c7); border-color:var(--warning,#d97706); color:var(--warning); }
.feature-body { flex:1; }
.feature-name { font-size:13px; font-weight:600; }
.feature-desc { font-size:12px; color:var(--muted-fg); margin-top:2px; }
hr.section-sep { border:none; border-top:1px solid var(--border); margin:0; }
</style>
</head>
<body>
<div class="page">
<div class="page-header">
<h1>📋 Kế hoạch phát triển</h1>
<p>Roadmap phát triển hệ thống Canifa AI Platform</p>
</div>
<!-- v2.5.0 - Current -->
<div class="version-block">
<div class="version-title">
<h2>v2.5.0</h2>
<span class="version-badge badge-current">Current</span>
<span class="version-date">Mar 2026</span>
</div>
<ul class="feature-list">
<li class="feature-item"><div class="feature-check done"></div><div class="feature-body"><div class="feature-name">AI SQL Agent (Text-to-SQL)</div><div class="feature-desc">Truy vấn dữ liệu bằng ngôn ngữ tự nhiên với Human-in-the-Loop</div></div></li>
<li class="feature-item"><div class="feature-check done"></div><div class="feature-body"><div class="feature-name">Memos-inspired UI Redesign</div><div class="feature-desc">Theme mới, modular CSS, Settings modal giống Memos</div></div></li>
<li class="feature-item"><div class="feature-check done"></div><div class="feature-body"><div class="feature-name">Prompt Optimizer</div><div class="feature-desc">Tối ưu prompt tự động với AI feedback loop</div></div></li>
<li class="feature-item"><div class="feature-check done"></div><div class="feature-body"><div class="feature-name">User Simulator & Stress Test</div><div class="feature-desc">Mô phỏng user và stress test hệ thống</div></div></li>
<li class="feature-item"><div class="feature-check done"></div><div class="feature-body"><div class="feature-name">Realtime Monitor</div><div class="feature-desc">Dashboard theo dõi hệ thống real-time</div></div></li>
<li class="feature-item"><div class="feature-check wip"></div><div class="feature-body"><div class="feature-name">History Viewer v2</div><div class="feature-desc">Date picker, product cards, cải thiện UX xem lịch sử</div></div></li>
</ul>
</div>
<hr class="section-sep">
<!-- v2.6.0 - Next -->
<div class="version-block" style="margin-top:28px;">
<div class="version-title">
<h2>v2.6.0</h2>
<span class="version-badge badge-next">Planned</span>
<span class="version-date">Q2 2026</span>
</div>
<ul class="feature-list">
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">Multi-Agent Orchestration</div><div class="feature-desc">Nhiều agent phối hợp: SQL Agent, Report Agent, Analytics Agent</div></div></li>
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">AI Data Analyst v2</div><div class="feature-desc">Tự động phân tích dữ liệu và tạo báo cáo insights</div></div></li>
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">Compact Sidebar (Memos-style)</div><div class="feature-desc">Sidebar thu gọn icon-only, expand on hover</div></div></li>
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">Dark Mode</div><div class="feature-desc">Theme tối cho platform</div></div></li>
</ul>
</div>
<hr class="section-sep">
<!-- v3.0.0 - Future -->
<div class="version-block" style="margin-top:28px;">
<div class="version-title">
<h2>v3.0.0</h2>
<span class="version-badge badge-future">Future</span>
<span class="version-date">Q3-Q4 2026</span>
</div>
<ul class="feature-list">
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">Customer Analytics Dashboard</div><div class="feature-desc">Phân tích hành vi khách hàng từ chat data</div></div></li>
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">A/B Testing Engine</div><div class="feature-desc">So sánh prompt/model variants trên live traffic</div></div></li>
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">Self-Learning System</div><div class="feature-desc">Auto-tune prompts dựa trên feedback data</div></div></li>
<li class="feature-item"><div class="feature-check"></div><div class="feature-body"><div class="feature-name">Mobile Admin App</div><div class="feature-desc">Quản lý chatbot từ điện thoại</div></div></li>
</ul>
</div>
</div>
</body>
</html>
<!-- <!DOCTYPE html>
<!-- <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canifa Chatbot Test</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
......@@ -588,7 +590,7 @@
<div class="container">
<div class="chat-internal-wrapper">
<div class="header">
<h2>🤖 Canifa AI Chat</h2>
<h2>?? Canifa AI Chat</h2>
<div class="config-area" style="flex-wrap: wrap; display: flex; align-items: center; gap: 10px;">
<div style="display: flex; gap: 5px; align-items: center;">
<label style="font-size: 0.8em; color: #aaa;">Device ID:</label>
......@@ -606,33 +608,33 @@
</div>
<!-- Action Buttons -->
<button onclick="loadHistory(true)" title="Load History"> History</button>
<button onclick="loadHistory(true)" title="Load History">? History</button>
<button onclick="togglePromptEditor()"
style="background: #e6b800; color: #2d2d2d; font-weight: bold;">📝 Prompt</button>
<button onclick="clearUI()" style="background: #d32f2f;"> UI</button>
style="background: #e6b800; color: #2d2d2d; font-weight: bold;">?? Prompt</button>
<button onclick="clearUI()" style="background: #d32f2f;">? UI</button>
</div>
</div>
<div class="chat-box" id="chatBox">
<div class="load-more" id="loadMoreBtn" style="display: none;">
<button onclick="loadHistory(false)">Load Older Messages ⬆️</button>
<button onclick="loadHistory(false)">Load Older Messages ??</button>
</div>
<div id="messagesArea" style="display: flex; flex-direction: column; gap: 15px;"></div>
</div>
<div class="typing-indicator" id="typingIndicator">
<span style="font-style: normal;">🤖</span> AI is thinking...
<span style="font-style: normal;">??</span> AI is thinking...
</div>
<div class="input-area">
<input type="text" id="userInput" placeholder="Type your message..."
onkeypress="handleKeyPress(event)" autocomplete="off">
<button onclick="sendMessage()" id="sendBtn"> Send</button>
<button onclick="sendMessage()" id="sendBtn">? Send</button>
<button onclick="resetChat()" id="resetBtn" title="Reset Session"
style="background: #ffc107; color: #333; font-weight: bold; padding: 0 20px; margin-left: 10px; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center;">🔄
style="background: #ffc107; color: #333; font-weight: bold; padding: 0 20px; margin-left: 10px; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center;">??
Reset</button>
</div>
</div>
......@@ -641,8 +643,8 @@
<!-- Prompt Editor Panel -->
<div class="prompt-panel" id="promptPanel">
<div class="prompt-header">
<h3>📝 System Prompt</h3>
<button class="btn-close-panel" onclick="togglePromptEditor()">×</button>
<h3>?? System Prompt</h3>
<button class="btn-close-panel" onclick="togglePromptEditor()"></button>
</div>
<textarea id="systemPromptInput" class="prompt-textarea" placeholder="Loading prompt content..."
......@@ -651,8 +653,8 @@
<div class="panel-footer">
<span class="status-text" id="promptStatus">Ready to edit</span>
<div style="display: flex; gap: 10px;">
<button class="action-btn btn-reload" onclick="loadSystemPrompt()"> Reset</button>
<button class="action-btn btn-save" onclick="saveSystemPrompt()">💾 Save & Apply</button>
<button class="action-btn btn-reload" onclick="loadSystemPrompt()">? Reset</button>
<button class="action-btn btn-save" onclick="saveSystemPrompt()">?? Save & Apply</button>
</div>
</div>
</div>
......@@ -663,7 +665,7 @@
let isPromptPanelOpen = false;
async function resetChat() {
if (!confirm('Bạn có chắc muốn làm mới cuộc trò chuyện? Lịch sử cũ sẽ được lưu trữ.')) return;
if (!confirm('B?n c ch?c mu?n lm m?i cu?c tr chuy?n? L?ch s? cu s? du?c luu tr?.')) return;
const deviceId = document.getElementById('deviceId').value;
if (!deviceId) return alert("Missing Device ID");
......@@ -682,16 +684,16 @@
if (response.ok && data.status === 'success') {
document.getElementById('messagesArea').innerHTML = '';
const remaining = data.remaining_resets !== undefined ? ` (Còn ${data.remaining_resets} lượt)` : '';
alert('✅ ' + (data.message || 'Reset thành công!') + remaining);
const remaining = data.remaining_resets !== undefined ? ` (Cn ${data.remaining_resets} lu?t)` : '';
alert('? ' + (data.message || 'Reset thnh cng!') + remaining);
} else {
const errorMsg = data.message || data.detail || 'Không thể reset';
const prefix = data.error_code === 'RESET_LIMIT_EXCEEDED' ? '⚠️ ' : '❌ Lỗi: ';
const errorMsg = data.message || data.detail || 'Khng th? reset';
const prefix = data.error_code === 'RESET_LIMIT_EXCEEDED' ? '?? ' : '? L?i: ';
alert(prefix + errorMsg);
}
} catch (error) {
console.error('Reset error:', error);
alert('Có lỗi xảy ra khi reset.');
alert('C l?i x?y ra khi reset.');
}
}
......@@ -734,7 +736,7 @@
const statusLabel = document.getElementById('promptStatus');
if (!content) return;
if (!confirm('Bạn có chắc muốn lưu Prompt mới? Bot sẽ bị reset graph để học prompt mới này.')) {
if (!confirm('B?n c ch?c mu?n luu Prompt m?i? Bot s? b? reset graph d? h?c prompt m?i ny.')) {
return;
}
......@@ -750,14 +752,14 @@
if (data.status === 'success') {
statusLabel.innerText = "Saved!";
alert('✅ Đã lưu Prompt thành công!\nBot đã sẵn sàng với prompt mới.');
alert('? luu Prompt thnh cng!\nBot d s?n sng v?i prompt m?i.');
} else {
statusLabel.innerText = "Error!";
alert('❌ Lỗi: ' + data.detail);
alert('? L?i: ' + data.detail);
}
} catch (error) {
statusLabel.innerText = "Connection Error";
alert('❌ Lỗi kết nối server');
alert('? L?i k?t n?i server');
console.error(error);
}
}
......@@ -801,10 +803,10 @@
currentCursor = null;
}
// Gọi API với device_id trong URL, nhưng gửi kèm headers để middleware resolve đúng identity
// G?i API v?i device_id trong URL, nhung g?i km headers d? middleware resolve dng identity
const url = `/api/history/${deviceId}?limit=20${currentCursor ? `&before_id=${currentCursor}` : ''}`;
// Build headers for identity resolution (middleware sẽ dùng token để override nếu có)
// Build headers for identity resolution (middleware s? dng token d? override n?u c)
const headers = {
'Content-Type': 'application/json',
'device_id': deviceId
......@@ -824,7 +826,7 @@
currentCursor = cursor;
if (isRefresh) {
// Refresh: reverse để oldest ở trên, newest ở dưới
// Refresh: reverse d? oldest ? trn, newest ? du?i
const batch = [...messages].reverse();
batch.forEach(msg => appendMessage(msg, 'bottom'));
setTimeout(() => {
......@@ -832,7 +834,7 @@
chatBox.scrollTop = chatBox.scrollHeight;
}, 100);
} else {
// Load more: messages từ API theo DESC (newest first của batch cũ)
// Load more: messages t? API theo DESC (newest first c?a batch cu)
const chatBox = document.getElementById('chatBox');
const oldHeight = chatBox.scrollHeight;
......@@ -918,12 +920,12 @@
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.innerText = '?? Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.innerText = '??? Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
......@@ -1035,12 +1037,12 @@
// Extract message
const errorMessage = errorData.message ||
errorData.detail?.message ||
'Có lỗi xảy ra!';
'C l?i x?y ra!';
filteredDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px;">⚠️ ${errorData.error_code || 'ERROR'}</div>
<div style="font-weight: bold; margin-bottom: 8px;">?? ${errorData.error_code || 'ERROR'}</div>
<div>${errorMessage}</div>
${errorData.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">👉 Đăng nhập ngay để tiếp tục!</div>' : ''}
${errorData.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">?? ang nh?p ngay d? ti?p t?c!</div>' : ''}
`;
botMsgDiv.appendChild(filteredDiv);
......@@ -1065,12 +1067,12 @@
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.innerText = '?? Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.innerText = '??? Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
......@@ -1080,7 +1082,7 @@
// Response time
const timeDiv = document.createElement('div');
timeDiv.className = 'response-time';
timeDiv.innerText = `⏱️ ${responseTime}s`;
timeDiv.innerText = `?? ${responseTime}s`;
botMsgDiv.appendChild(timeDiv);
container.appendChild(botMsgDiv);
......@@ -1095,7 +1097,7 @@
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail?.message || errorData.detail || 'Có lỗi xảy ra');
throw new Error(errorData.detail?.message || errorData.detail || 'C l?i x?y ra');
}
const data = await response.json();
......@@ -1178,19 +1180,19 @@
// Show original price with strikethrough
const originalPrice = document.createElement('span');
originalPrice.className = 'price-original';
originalPrice.innerText = product.price.toLocaleString('vi-VN') + 'đ';
originalPrice.innerText = product.price.toLocaleString('vi-VN') + 'd';
priceDiv.appendChild(originalPrice);
// Show sale price
const salePrice = document.createElement('span');
salePrice.className = 'price-sale';
salePrice.innerText = product.sale_price.toLocaleString('vi-VN') + 'đ';
salePrice.innerText = product.sale_price.toLocaleString('vi-VN') + 'd';
priceDiv.appendChild(salePrice);
} else {
// Show regular price
const regularPrice = document.createElement('span');
regularPrice.className = 'price-regular';
regularPrice.innerText = product.price.toLocaleString('vi-VN') + 'đ';
regularPrice.innerText = product.price.toLocaleString('vi-VN') + 'd';
priceDiv.appendChild(regularPrice);
}
body.appendChild(priceDiv);
......@@ -1200,7 +1202,7 @@
link.className = 'product-link';
link.href = product.url;
link.target = '_blank';
link.innerText = '🛍️ Xem chi tiết';
link.innerText = '??? Xem chi ti?t';
body.appendChild(link);
card.appendChild(body);
......@@ -1238,12 +1240,12 @@
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.innerText = '?? Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.innerText = '??? Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
......@@ -1253,7 +1255,7 @@
// Add response time
const timeDiv = document.createElement('div');
timeDiv.className = 'response-time';
timeDiv.innerText = `⏱️ ${responseTime}s`;
timeDiv.innerText = `?? ${responseTime}s`;
botMsgDiv.appendChild(timeDiv);
} else {
// ERROR CASE: Limit exceeded or other errors
......@@ -1264,9 +1266,9 @@
filteredDiv.className = 'filtered-content';
filteredDiv.style.color = '#ff6b6b';
filteredDiv.innerHTML = `
<div style="font-weight: bold; margin-bottom: 8px;">⚠️ ${data.error_code || 'ERROR'}</div>
<div style="font-weight: bold; margin-bottom: 8px;">?? ${data.error_code || 'ERROR'}</div>
<div>${data.message || 'Unknown error'}</div>
${data.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">👉 Vui lòng đăng nhập để tiếp tục sử dụng!</div>' : ''}
${data.require_login ? '<div style="margin-top: 10px; padding: 8px; background: #3d2d2d; border-radius: 6px;">?? Vui lng dang nh?p d? ti?p t?c s? d?ng!</div>' : ''}
`;
botMsgDiv.appendChild(filteredDiv);
......@@ -1297,12 +1299,12 @@
const filteredBtn = document.createElement('button');
filteredBtn.id = 'filtered-btn-' + messageId;
filteredBtn.className = 'active';
filteredBtn.innerText = '🎨 Widget';
filteredBtn.innerText = '?? Widget';
filteredBtn.onclick = () => toggleMessageView(messageId);
const rawBtn = document.createElement('button');
rawBtn.id = 'raw-btn-' + messageId;
rawBtn.innerText = '👁️ Raw JSON';
rawBtn.innerText = '??? Raw JSON';
rawBtn.onclick = () => toggleMessageView(messageId);
toggleDiv.appendChild(filteredBtn);
......@@ -1347,9 +1349,9 @@
if (tokenInput && tokenInput.value.trim()) {
document.getElementById('accessToken').value = tokenInput.value.trim();
saveConfig();
alert('✅ Token đã được lưu! Bạn có thể tiếp tục chat.');
alert('? Token d du?c luu! B?n c th? ti?p t?c chat.');
} else {
alert('Vui lòng nhập Access Token!');
alert('Vui lng nh?p Access Token!');
}
}
......
......@@ -5,9 +5,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔍 DB Direct Query Tester</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* {
box-sizing: border-box;
......
......@@ -5,9 +5,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🤖 Text-to-SQL Chatbot — Bản 2</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/warm-override.css">
<script src="/static/frame-detect.js"></script>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<link rel="stylesheet" href="/static/css/warm-override.css">
<script src="/static/js/frame-detect.js"></script>
<style>
* {
box-sizing: border-box;
......
......@@ -4,7 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Insight — CDP</title>
<link rel="stylesheet" href="/static/lab.css">
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/lab.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
*{box-sizing:border-box;margin:0;padding:0}
......@@ -179,7 +181,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:#F8F9FC;min-height:100v
<div class="profile-content" id="profileContent" style="display:none"></div>
</div>
</div>
<script src="/static/user-insight-data.js"></script>
<script src="/static/user-insight-render.js"></script>
<script src="/static/js/user-insight-data.js"></script>
<script src="/static/js/user-insight-render.js"></script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment