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

feat(ui): extract CSS to index.css + premium light-mode redesign

parent 3e2fa498
/* ── DESIGN TOKENS (Foundation) ── */
:root {
/* Surface (Premium Light Mode) */
--surface-base: #ffffff;
--surface-muted: #f7f7f9; /* Subtle off-white */
--surface-raised: #ffffff;
--surface-strong: #f0f0f4;
/* Border */
--border-default: #e5e5eb;
--border-muted: #d1d1d8;
--border-focus: #a0a0ab;
/* Text */
--text-primary: #111111; /* Deep charcoal */
--text-secondary: #555555;
--text-tertiary: #888888;
--text-inverse: #ffffff;
/* Brand */
--brand: #e01830; /* Vibrant Canifa Red */
--brand-hover: #c8102e;
--brand-subtle: rgba(224, 24, 48, 0.08);
--brand-border: rgba(224, 24, 48, 0.2);
/* Semantic */
--success: #2bc270;
--warning: #f5a623;
--error: #e01830;
/* Radius */
--r-xs: 6px;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
--r-xl: 20px;
--r-full: 9999px;
/* Spacing */
--sp1: 2px; --sp2: 4px; --sp3: 6px; --sp4: 8px;
--sp5: 12px; --sp6: 16px; --sp7: 20px; --sp8: 24px;
/* Typography */
--font: 'Geist', 'Geist Fallback', system-ui, sans-serif;
--font-mono: 'Geist Mono', 'Geist Fallback', monospace;
--text-xs: 13px;
--text-sm: 14px;
--text-md: 15px;
--text-lg: 18px;
--text-xl: 22px;
--text-2xl: 48px;
--lh-base: 1.5;
/* Shadow (Soft & Elegant for Light Mode) */
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 4px 12px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 24px rgba(0,0,0,0.08);
--ring: 0 0 0 1px rgba(0,0,0,0.05);
--ring-focus: 0 0 0 3px rgba(0,0,0,0.1);
--ring-brand: 0 0 0 3px rgba(224, 24, 48, 0.2);
/* Motion */
--dur-instant: 100ms;
--dur-fast: 200ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
/* ── RESET ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%; overflow: hidden;
background: var(--surface-muted);
color: var(--text-primary);
font-family: var(--font);
font-size: var(--text-md);
font-weight: 500;
line-height: var(--lh-base);
-webkit-font-smoothing: antialiased;
}
/* ── SCROLLBAR ── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: var(--r-full); }
::-webkit-scrollbar-thumb:hover { background: var(--border-muted); }
* { scrollbar-width: thin; scrollbar-color: var(--border-default) transparent; }
/* ── FOCUS VISIBLE ── */
:focus-visible { outline: none; box-shadow: var(--ring-focus); border-radius: var(--r-sm); }
/* ── LAYOUT ── */
.app {
display: grid;
grid-template-columns: 1fr 320px;
height: 100vh;
overflow: hidden;
background: var(--surface-base);
}
/* ══════════════════════════════
CHAT PANEL
══════════════════════════════ */
.chat-panel {
display: flex;
flex-direction: column;
min-width: 0;
border-right: 1px solid var(--border-default);
overflow: hidden;
background: var(--surface-base);
}
/* HEADER */
.hdr {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sp6) var(--sp8);
border-bottom: 1px solid var(--border-default);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
flex-shrink: 0;
gap: var(--sp7);
z-index: 10;
}
.brand {
display: flex;
align-items: center;
gap: var(--sp5);
flex-shrink: 0;
}
.brand-icon {
width: 32px; height: 32px;
background: var(--brand);
border-radius: var(--r-sm);
display: grid;
place-items: center;
flex-shrink: 0;
box-shadow: 0 4px 10px var(--brand-subtle);
}
.brand-icon svg { width: 16px; height: 16px; fill: #fff; }
.brand-name {
font-size: var(--text-md);
font-weight: 700;
color: var(--text-primary);
letter-spacing: .02em;
white-space: nowrap;
}
.brand-dot {
width: 6px; height: 6px;
background: var(--success);
border-radius: 50%;
flex-shrink: 0;
animation: pulse 2.4s ease infinite;
}
@keyframes pulse { 0%,100%{opacity:1; box-shadow: 0 0 0 0 rgba(43,194,112,0.4)} 50%{opacity:.7; box-shadow: 0 0 0 4px rgba(43,194,112,0)} }
.hdr-right { display: flex; align-items: center; gap: var(--sp5); }
/* INPUTS */
.field {
display: flex;
align-items: center;
gap: var(--sp3);
height: 32px;
padding: 0 var(--sp5);
background: var(--surface-muted);
border: 1px solid var(--border-default);
border-radius: var(--r-md);
transition: all var(--dur-fast) var(--ease-out);
}
.field:focus-within {
background: var(--surface-base);
border-color: var(--border-focus);
box-shadow: var(--ring-focus);
}
.field input {
background: transparent;
border: none;
outline: none;
font: 500 var(--text-sm)/1 var(--font-mono);
color: var(--text-primary);
width: 110px;
}
.field input::placeholder { color: var(--text-tertiary); }
.field-label {
font-size: var(--text-xs);
color: var(--text-tertiary);
font-weight: 600;
white-space: nowrap;
user-select: none;
}
/* TOGGLE */
.toggle-wrap {
display: flex; align-items: center;
gap: var(--sp3); cursor: pointer; user-select: none;
padding: 4px 8px;
border-radius: var(--r-md);
transition: background var(--dur-fast);
}
.toggle-wrap:hover { background: var(--surface-muted); }
.toggle-wrap span {
font-size: var(--text-xs);
color: var(--text-secondary);
font-weight: 600;
}
.toggle {
position: relative;
width: 36px; height: 20px;
display: inline-block;
}
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-track {
position: absolute; inset: 0;
background: var(--surface-strong);
border-radius: var(--r-full);
cursor: pointer;
transition: background var(--dur-fast) var(--ease-out);
}
.toggle-track::after {
content: '';
position: absolute;
width: 16px; height: 16px;
top: 2px; left: 2px;
background: #fff;
border-radius: 50%;
box-shadow: var(--shadow-sm);
transition: transform var(--dur-fast) var(--ease-out);
}
.toggle input:checked + .toggle-track { background: var(--success); }
.toggle input:checked + .toggle-track::after { transform: translateX(16px); }
.toggle input:focus-visible + .toggle-track { box-shadow: var(--ring-focus); }
/* GHOST BUTTON */
.btn-ghost {
height: 32px;
padding: 0 var(--sp5);
background: transparent;
border: 1px solid var(--border-default);
border-radius: var(--r-md);
font: 600 var(--text-xs)/1 var(--font);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--dur-fast) var(--ease-out);
white-space: nowrap;
}
.btn-ghost:hover { border-color: var(--border-muted); color: var(--text-primary); background: var(--surface-muted); }
.btn-ghost:active { background: var(--surface-strong); transform: translateY(1px); }
.btn-ghost:focus-visible { box-shadow: var(--ring-focus); outline: none; }
/* ── MESSAGES SCROLL ── */
.chat-scroll {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
min-height: 0;
background: var(--surface-muted); /* Light gray bg for chat area */
}
#msgs {
width: 100%;
max-width: 800px;
padding: 32px var(--sp8);
display: flex;
flex-direction: column;
gap: 24px;
}
/* WELCOME */
.welcome {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp6);
padding: 64px var(--sp8);
text-align: center;
}
.welcome-mark {
width: 56px; height: 56px;
background: #ffffff;
border: 1px solid var(--brand-border);
border-radius: var(--r-xl);
display: grid;
place-items: center;
box-shadow: var(--shadow-md);
}
.welcome-mark svg { width: 28px; height: 28px; fill: var(--brand); }
.welcome h1 {
font-size: var(--text-xl);
font-weight: 700;
color: var(--text-primary);
letter-spacing: -.02em;
}
.welcome p { font-size: var(--text-md); color: var(--text-secondary); max-width: 400px; }
.chip-row {
display: flex;
flex-wrap: wrap;
gap: var(--sp4);
justify-content: center;
margin-top: var(--sp5);
}
.chip {
height: 36px;
padding: 0 var(--sp6);
background: #ffffff;
border: 1px solid var(--border-default);
border-radius: var(--r-full);
font: 500 var(--text-sm)/34px var(--font);
color: var(--text-secondary);
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: all var(--dur-fast) var(--ease-out);
white-space: nowrap;
}
.chip:hover {
border-color: var(--brand-border);
color: var(--brand);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.chip:focus-visible { outline: none; box-shadow: var(--ring-brand); }
/* MSG ROW */
.msg-row { display: flex; flex-direction: column; }
.msg-row.user { align-items: flex-end; }
.msg-row.bot { align-items: flex-start; }
.bubble {
max-width: 80%;
padding: var(--sp6) var(--sp7);
font-size: var(--text-md);
line-height: 1.6;
animation: slideUp var(--dur-fast) var(--ease-out) both;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.bubble.user {
background: var(--text-primary);
color: #ffffff;
border-radius: var(--r-xl) var(--r-xl) var(--r-xs) var(--r-xl);
box-shadow: var(--shadow-sm);
}
.bubble.bot {
background: #ffffff;
color: var(--text-primary);
border: 1px solid var(--border-default);
border-radius: var(--r-xl) var(--r-xl) var(--r-xl) var(--r-xs);
box-shadow: var(--shadow-sm);
}
.msg-ts {
margin-top: var(--sp3);
font: 500 var(--text-xs)/1 var(--font-mono);
color: var(--text-tertiary);
margin-left: var(--sp2);
margin-right: var(--sp2);
}
/* TYPING */
.typing-dots {
display: flex; gap: 4px; align-items: center;
padding: var(--sp4) var(--sp2);
}
.typing-dots i {
width: 6px; height: 6px;
background: var(--text-tertiary);
border-radius: 50%;
animation: blink 1.4s infinite;
}
.typing-dots i:nth-child(2) { animation-delay: .2s; }
.typing-dots i:nth-child(3) { animation-delay: .4s; }
@keyframes blink { 0%,80%,100%{opacity:.3; transform: scale(1)} 40%{opacity:1; transform: scale(1.2)} }
/* PRODUCT STRIP */
.product-strip {
display: flex;
gap: var(--sp5);
margin-top: var(--sp6);
overflow-x: auto;
padding: 4px; /* for shadow */
padding-bottom: var(--sp5);
}
.p-card {
flex: 0 0 160px;
background: #ffffff;
border: 1px solid var(--border-default);
border-radius: var(--r-md);
overflow: hidden;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-sm);
transition: all var(--dur-fast) var(--ease-out);
}
.p-card:hover {
border-color: var(--border-muted);
transform: translateY(-4px);
box-shadow: var(--shadow-md);
}
.p-card:focus-visible { outline: none; box-shadow: var(--ring-focus); }
.p-img {
aspect-ratio: 3/4;
background: var(--surface-muted);
overflow: hidden;
}
.p-img img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
transition: transform 0.5s var(--ease-out);
}
.p-card:hover .p-img img { transform: scale(1.05); }
.p-meta {
padding: var(--sp5);
display: flex;
flex-direction: column;
flex: 1;
}
.p-name {
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-secondary);
line-height: 1.4;
height: 2.8em;
overflow: hidden;
margin-bottom: var(--sp4);
}
.p-price-row {
margin-top: auto;
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: var(--sp2);
}
.p-price {
font: 700 var(--text-md)/1 var(--font-mono);
color: var(--text-primary);
}
.p-old {
font-size: var(--text-xs);
text-decoration: line-through;
color: var(--text-tertiary);
}
/* ── INPUT BAR ── */
.inp-bar {
padding: var(--sp6) var(--sp8);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
border-top: 1px solid var(--border-default);
display: flex;
justify-content: center;
flex-shrink: 0;
z-index: 10;
}
.inp-wrap {
width: 100%;
max-width: 800px;
display: flex;
align-items: center;
gap: var(--sp5);
min-height: 52px;
padding: var(--sp3) var(--sp4) var(--sp3) var(--sp7);
background: #ffffff;
border: 1px solid var(--border-default);
border-radius: var(--r-xl);
box-shadow: var(--shadow-sm);
transition: all var(--dur-fast) var(--ease-out);
}
.inp-wrap:focus-within {
border-color: var(--border-muted);
box-shadow: var(--shadow-md);
}
.inp-wrap input {
flex: 1;
background: transparent;
border: none;
outline: none;
font: 500 var(--text-md)/1.5 var(--font);
color: var(--text-primary);
}
.inp-wrap input::placeholder { color: var(--text-tertiary); }
.btn-send {
width: 36px; height: 36px;
background: var(--brand);
border: none;
border-radius: var(--r-md);
cursor: pointer;
display: grid;
place-items: center;
flex-shrink: 0;
transition: all var(--dur-fast) var(--ease-out);
box-shadow: 0 2px 6px var(--brand-subtle);
}
.btn-send:hover { background: var(--brand-hover); box-shadow: 0 4px 12px var(--brand-border); }
.btn-send:active { transform: scale(.95); }
.btn-send:focus-visible { outline: none; box-shadow: var(--ring-brand); }
.btn-send svg { width: 16px; height: 16px; fill: #fff; }
/* ══════════════════════════════
SIDEBAR
══════════════════════════════ */
.sidebar {
display: flex;
flex-direction: column;
background: var(--surface-muted); /* slightly off-white */
border-left: 1px solid var(--border-default);
overflow: hidden;
}
.sb-hdr {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sp6) var(--sp7);
border-bottom: 1px solid var(--border-default);
background: #ffffff;
flex-shrink: 0;
}
.sb-hdr-title {
font-size: var(--text-sm);
font-weight: 700;
color: var(--text-primary);
letter-spacing: .02em;
}
.sb-body {
flex: 1;
overflow-y: auto;
padding: var(--sp7);
display: flex;
flex-direction: column;
gap: var(--sp6);
}
/* SIDEBAR CARD */
.sc {
background: #ffffff;
border: 1px solid var(--border-default);
border-radius: var(--r-md);
padding: var(--sp6);
box-shadow: var(--shadow-sm);
}
.sc-title {
font-size: var(--text-xs);
font-weight: 700;
color: var(--text-secondary);
letter-spacing: .05em;
text-transform: uppercase;
margin-bottom: var(--sp5);
display: flex;
align-items: center;
gap: var(--sp3);
}
.sc-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sp4) 0;
border-bottom: 1px solid var(--border-default);
font-size: var(--text-sm);
}
.sc-row:last-child { border-bottom: none; padding-bottom: 0; }
.sc-label { color: var(--text-secondary); font-weight: 500; }
.sc-val {
font: 600 var(--text-sm)/1 var(--font-mono);
color: var(--text-primary);
}
/* BADGE */
.badge {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 var(--sp4);
border-radius: var(--r-sm);
font: 700 11px/1 var(--font);
letter-spacing: .02em;
text-transform: uppercase;
white-space: nowrap;
}
.badge-brand { background: var(--brand-subtle); color: var(--brand); }
.badge-green { background: rgba(43,194,112,.15); color: #209353; }
.badge-amber { background: rgba(255,159,10,.15); color: #c47600; }
/* PERF GRID */
.perf-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp4); }
.perf-cell {
background: var(--surface-muted);
border: 1px solid var(--border-default);
border-radius: var(--r-sm);
padding: var(--sp5);
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.perf-val {
display: block;
font: 700 20px/1 var(--font-mono);
color: var(--brand);
margin-bottom: var(--sp3);
}
.perf-lbl { font-size: var(--text-xs); font-weight: 600; color: var(--text-tertiary); letter-spacing: .02em; text-transform: uppercase; }
/* PROMPT EDITOR */
.ptabs { display: flex; gap: var(--sp3); margin-bottom: var(--sp5); }
.ptab {
flex: 1;
height: 32px;
background: var(--surface-muted);
border: 1px solid var(--border-default);
border-radius: var(--r-sm);
font: 600 var(--text-xs)/1 var(--font-mono);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--dur-fast);
letter-spacing: .02em;
}
.ptab:hover { border-color: var(--border-muted); background: var(--surface-strong); }
.ptab.active { background: var(--text-primary); border-color: var(--text-primary); color: #fff; }
.ptab:focus-visible { outline: none; box-shadow: var(--ring-focus); }
.ptarea {
width: 100%;
height: 200px;
background: #ffffff;
border: 1px solid var(--border-default);
border-radius: var(--r-sm);
padding: var(--sp5);
font: 500 var(--text-xs)/1.6 var(--font-mono);
color: #005cc5; /* Code blue */
resize: vertical;
outline: none;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}
.ptarea:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-subtle); }
.btn-primary {
width: 100%;
margin-top: var(--sp6);
height: 36px;
background: var(--brand);
border: none;
border-radius: var(--r-sm);
font: 600 var(--text-sm)/1 var(--font);
color: #fff;
cursor: pointer;
letter-spacing: .02em;
transition: all var(--dur-fast) var(--ease-out);
box-shadow: 0 2px 6px var(--brand-subtle);
}
.btn-primary:hover { background: var(--brand-hover); box-shadow: 0 4px 12px var(--brand-border); }
.btn-primary:active { transform: translateY(1px); box-shadow: none; }
.btn-primary:focus-visible { outline: none; box-shadow: var(--ring-brand); }
/* DIVIDER */
.divider {
height: 1px;
background: var(--border-default);
margin: 0 calc(-1 * var(--sp6));
}
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CANIFA STYLIST PRO</title>
<!-- Google Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@400;600;700;800&display=swap">
<style>
:root {
--primary: #C8102E;
--primary-hover: #A30D25;
--bg-main: #F2F4F7;
--bg-surface: #FFFFFF;
--bg-bubble-user: #C8102E;
--bg-bubble-ai: #FFFFFF; /* Pure white for AI bubbles */
--text-main: #101828;
--text-secondary: #475467;
--text-muted: #667085;
--border: #E4E7EC;
--radius-bubble: 6px;
--font-sans: 'Inter', sans-serif;
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-sans);
background-color: var(--bg-main);
color: var(--text-main);
height: 100dvh; /* Dynamic viewport height */
margin: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ═══ MAIN LAYOUT ═══ */
.app-container {
width: 100%;
height: 100%;
display: flex; /* Flex instead of Grid for better 'relative' behavior */
overflow: hidden;
}
.chat-panel {
flex: 1;
background: white;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden; /* Contain children strictly */
border-right: 1px solid var(--border);
}
.chat-header {
padding: 12px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: white;
z-index: 10;
}
.config-inputs { display: flex; gap: 8px; }
.config-inputs input { padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 11px; width: 120px; outline: none; transition: 0.2s; }
.config-inputs input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-glow); }
.chat-box {
flex: 1;
overflow-y: auto;
background: #F9FAFB;
display: flex;
flex-direction: column;
align-items: center;
scroll-behavior: smooth;
min-height: 0;
}
#messagesArea {
width: 95%; /* RELATIVE % WIDTH */
max-width: 800px;
padding: 30px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.msg-container { width: 100%; display: flex; flex-direction: column; }
.msg-container.user { align-items: flex-end; }
.msg-container.bot { align-items: flex-start; }
.bubble {
max-width: 85%;
padding: 12px 16px;
border-radius: var(--radius-bubble);
font-size: 14px;
line-height: 1.6;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
position: relative;
}
.bubble.user {
background: var(--bg-bubble-user);
color: white;
border-color: var(--primary);
border-bottom-right-radius: 0;
}
.bubble.bot {
background: var(--bg-bubble-ai);
color: var(--text-main);
border-bottom-left-radius: 0;
}
.timestamp { font-size: 10px; color: var(--text-muted); margin-top: 6px; font-weight: 500; }
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CANIFA STYLIST PRO</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/font.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
/* PRODUCT ROW (Scrollable or Grid) */
.product-row {
display: flex;
gap: 12px;
margin-top: 12px;
overflow-x: auto;
padding-bottom: 10px;
width: 100%;
scrollbar-width: thin;
}
.p-card {
flex: 0 0 160px;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
background: white;
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
}
.p-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); border-color: var(--primary); }
.p-img-box { position: relative; width: 100%; aspect-ratio: 3/4; background: #F2F4F7; overflow: hidden; }
.p-img { width: 100%; height: 100%; object-fit: cover; transition: 0.3s; }
.p-card:hover .p-img { transform: scale(1.05); }
.p-info { padding: 10px; }
.p-name { font-size: 12px; font-weight: 600; color: var(--text-main); height: 34px; overflow: hidden; margin-bottom: 6px; line-height: 1.4; }
.p-price-row { display: flex; align-items: baseline; gap: 6px; }
.p-price { font-size: 13px; font-weight: 700; color: var(--primary); }
.p-price-old { font-size: 11px; text-decoration: line-through; color: var(--text-muted); }
/* INPUT AREA */
.input-area {
padding: 20px 24px;
background: white;
border-top: 1px solid var(--border);
display: flex;
justify-content: center;
z-index: 10;
}
.input-wrapper {
width: 100%;
max-width: 720px;
background: #FFFFFF;
border: 2px solid var(--border); /* STRONGER BORDER */
border-radius: 12px;
padding: 8px 16px;
display: flex;
gap: 12px;
align-items: center;
transition: 0.2s;
}
.input-wrapper:focus-within { border-color: var(--primary); box-shadow: 0 4px 12px rgba(200, 16, 46, 0.08); }
.input-wrapper input { flex: 1; background: transparent; border: none; font-size: 14px; outline: none; padding: 6px 0; color: var(--text-main); }
.btn-send {
background: var(--primary);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: 0.2s;
}
.btn-send:hover { background: var(--primary-hover); transform: scale(1.05); }
.btn-send svg { width: 22px; height: 22px; fill: white; }
<div class="app" role="main">
/* DEBUG PANEL */
.debug-panel {
width: 360px; /* FIXED WIDTH FOR SIDEBAR */
background: #F8F9FC;
padding: 20px;
display: flex;
flex-direction: column;
overflow-y: auto;
gap: 16px;
border-left: 1px solid var(--border);
}
.debug-card { background: white; border: 1px solid var(--border); border-radius: 12px; padding: 16px; box-shadow: var(--shadow-sm); }
.debug-card h4 { font-size: 11px; font-weight: 800; color: var(--text-muted); text-transform: uppercase; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.debug-card h4 svg { width: 14px; height: 14px; fill: var(--text-muted); }
.debug-row { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 8px; }
.debug-row .label { color: var(--text-secondary); }
.debug-row .value { color: var(--text-main); font-weight: 700; }
<!-- ══ CHAT PANEL ══ -->
<section class="chat-panel" aria-label="Chat">
.timing-box { background: #F9FAFB; border: 1px solid var(--border); padding: 10px; border-radius: 8px; text-align: center; }
.timing-box .val { font-size: 16px; font-weight: 800; color: var(--primary); }
.timing-box .lbl { font-size: 10px; color: var(--text-muted); }
<!-- Header -->
<header class="hdr">
<div class="brand">
<div class="brand-icon" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
</div>
<span class="brand-name">Canifa Stylist Pro</span>
<div class="brand-dot" role="status" aria-label="Online"></div>
</div>
<div class="hdr-right">
<label class="field" aria-label="Device ID">
<span class="field-label">DEV</span>
<input id="deviceId" type="text" placeholder="—" autocomplete="off" aria-label="Device ID">
</label>
<label class="field" aria-label="Access token">
<span class="field-label">TOKEN</span>
<input id="accessToken" type="password" placeholder="Optional" autocomplete="off" aria-label="Access token" style="width:76px">
</label>
<label class="toggle-wrap" aria-label="Toggle mock database">
<span>Mock DB</span>
<label class="toggle">
<input type="checkbox" id="mockMode" checked aria-label="Mock DB mode">
<span class="toggle-track"></span>
</label>
</label>
<button class="btn-ghost" onclick="resetChat()">Reset</button>
</div>
</header>
<!-- Messages -->
<div class="chat-scroll" id="chatBox" role="log" aria-live="polite" aria-label="Conversation">
<div id="msgs">
<div class="welcome" id="welcome">
<div class="welcome-mark" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M17 8C8 10 5.9 16.17 3.82 19.83 5.34 19.94 6.9 20 8.5 20c3.03 0 5.94-.28 8.5-.79V8zm3.93-.98C20.97 7.02 21 7.01 21 7c0-3.31-2.69-6-6-6-1.01 0-1.96.26-2.79.7L17.94 6.5c.81.64 1.94.9 2.99.52z"/></svg>
</div>
<h1>Canifa Stylist Pro</h1>
<p>AI fashion consultant — start with a question below</p>
<div class="chip-row" role="list" aria-label="Quick suggestions">
<button class="chip" role="listitem" onclick="quickSend('Tôi cần áo sơ mi đi làm')">Áo sơ mi công sở</button>
<button class="chip" role="listitem" onclick="quickSend('Outfit mùa hè cho nữ')">Outfit mùa hè</button>
<button class="chip" role="listitem" onclick="quickSend('Quần kaki nam slim fit')">Quần kaki nam</button>
<button class="chip" role="listitem" onclick="quickSend('Đồ đi tiệc sang trọng')">Đi tiệc</button>
<button class="chip" role="listitem" onclick="quickSend('Áo khoác thu đông')">Áo khoác</button>
</div>
</div>
</div>
</div>
.prompt-edit-area { width: 100%; height: 220px; background: #1a1a1a; color: #10b981; border-radius: 8px; padding: 12px; font-family: 'JetBrains Mono', monospace; font-size: 12px; border: none; margin-top: 10px; resize: none; }
.btn-save { width: 100%; padding: 12px; background: var(--primary); color: white; border: none; border-radius: 8px; font-size: 12px; font-weight: 700; cursor: pointer; margin-top: 10px; }
<!-- Input -->
<div class="inp-bar">
<div class="inp-wrap">
<input id="userInput" type="text"
placeholder="Ask about fashion or Canifa products…"
autocomplete="off"
aria-label="Message input"
onkeydown="if(event.key==='Enter')sendMessage()">
<button class="btn-send" onclick="sendMessage()" aria-label="Send message">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
</div>
</section>
/* TOGGLE SWITCH */
.switch-container { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; color: var(--text-secondary); }
.switch { position: relative; display: inline-block; width: 34px; height: 18px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 18px; }
.slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--primary); }
input:checked + .slider:before { transform: translateX(16px); }
<!-- ══ SIDEBAR ══ -->
<aside class="sidebar" aria-label="Debug and insights">
<div class="sb-hdr">
<span class="sb-hdr-title">Insights</span>
<span class="badge badge-green" id="status-badge">Live</span>
</div>
::-webkit-scrollbar { height: 6px; width: 10px; } /* WIDER SCROLLBAR */
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 10px; border: 2px solid #F9FAFB; }
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
</style>
</head>
<body>
<div class="sb-body">
<div class="app-container">
<div class="chat-panel">
<div class="chat-header">
<div class="config-inputs">
<input type="text" id="deviceId" placeholder="Device ID">
<input type="password" id="accessToken" placeholder="Token (Optional)">
<div class="switch-container">
<span>MOCK DB</span>
<label class="switch">
<input type="checkbox" id="mockMode">
<span class="slider"></span>
</label>
</div>
</div>
<button style="color:var(--primary); font-weight:700; font-size:11px; border:none; background:none; cursor:pointer;" onclick="resetChat()">RESET SESSION</button>
<!-- Session -->
<div class="sc" role="region" aria-label="Session info">
<div class="sc-title">Session</div>
<div class="sc-row">
<span class="sc-label">Stage</span>
<span id="dStage" class="badge badge-brand">Awareness</span>
</div>
<div class="chat-box" id="chatBox">
<div id="messagesArea">
<div style="text-align:center; padding: 50px 20px;">
<div style="width:60px; height:60px; background:var(--primary-glow); border-radius:12px; display:flex; align-items:center; justify-content:center; margin:0 auto 20px;">
<svg viewBox="0 0 24 24" style="width:32px; height:32px; fill:var(--primary);"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
</div>
<h2 style="font-family:'Outfit'; font-weight:800; font-size:22px; color:var(--primary); letter-spacing:1px;">CANIFA STYLIST PRO</h2>
<p style="font-size:14px; color:var(--text-secondary); margin-top:8px;">AI Personal Fashion Consultant</p>
</div>
</div>
<div class="sc-row">
<span class="sc-label">Tone</span>
<span class="sc-val">Stylist Pro</span>
</div>
<div class="input-area">
<div class="input-wrapper">
<input type="text" id="userInput" placeholder="Hỏi về thời trang hoặc sản phẩm..." autocomplete="off"
onkeydown="if(event.key==='Enter') sendMessage()">
<button class="btn-send" onclick="sendMessage()">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
<div class="sc-row">
<span class="sc-label">DB source</span>
<span class="sc-val" id="dDb">sqlite</span>
</div>
</div>
<div class="debug-panel">
<div class="debug-card">
<h4><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg> Insights</h4>
<div class="debug-row"><span class="label">Current Stage</span><span class="value" id="dStage">Awareness</span></div>
<div class="debug-row"><span class="label">Tone Policy</span><span class="value">Stylist Pro</span></div>
<div class="sc-row">
<span class="sc-label">Messages</span>
<span class="sc-val" id="dMsgs">0</span>
</div>
<div class="debug-card">
<h4><svg viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg> Performance</h4>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
<div class="timing-box"><div class="val" id="tLat"></div><div class="lbl">Latency</div></div>
<div class="timing-box"><div class="val" id="tTools"></div><div class="lbl">Tools</div></div>
</div>
</div>
<!-- Performance -->
<div class="sc" role="region" aria-label="Performance metrics">
<div class="sc-title">Performance</div>
<div class="perf-grid">
<div class="perf-cell">
<span class="perf-val" id="tLat"></span>
<span class="perf-lbl">Latency</span>
</div>
<div class="perf-cell">
<span class="perf-val" id="tTools"></span>
<span class="perf-lbl">Tools</span>
</div>
</div>
<div class="debug-card">
<h4><svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg> Prompt Editor</h4>
<div style="display:flex; gap:6px; margin-bottom:10px;">
<button onclick="loadPrompt('system')" style="flex:1; font-size:10px; padding:6px; border:1px solid var(--border); border-radius:4px; cursor:pointer; background:white; font-weight:700;">SYSTEM</button>
<button onclick="loadPrompt('tool')" style="flex:1; font-size:10px; padding:6px; border:1px solid var(--border); border-radius:4px; cursor:pointer; background:white; font-weight:700;">TOOL</button>
</div>
<textarea id="promptArea" class="prompt-edit-area" spellcheck="false"></textarea>
<button class="btn-save" onclick="savePrompt()">UPDATE ARCHITECTURE</button>
</div>
<!-- Prompt editor -->
<div class="sc" role="region" aria-label="Prompt editor">
<div class="sc-title">Prompt Editor</div>
<div class="ptabs" role="tablist">
<button class="ptab active" role="tab" aria-selected="true"
onclick="loadPrompt('system',this)">System</button>
<button class="ptab" role="tab" aria-selected="false"
onclick="loadPrompt('tool',this)">Tool</button>
</div>
<textarea id="promptArea" class="ptarea" spellcheck="false"
aria-label="Prompt content editor">You are CANIFA STYLIST PRO, an expert AI fashion consultant for Canifa — Vietnam's leading fashion brand.
Your role:
- Understand the customer's style needs
- Recommend matching Canifa products
- Provide personalized styling advice
- Guide through the purchase journey
Always respond warmly in Vietnamese.</textarea>
<button class="btn-primary" onclick="savePrompt()">Update Architecture</button>
</div>
</div>
</aside>
</div>
<script src="/static/service/api.js"></script>
<script>
function scrollToBottom() {
const chatBox = document.getElementById('chatBox');
if (!chatBox) return;
// Immediate scroll
chatBox.scrollTop = chatBox.scrollHeight;
// Delayed scrolls to catch image loads
[50, 150, 300, 600].forEach(delay => {
setTimeout(() => {
chatBox.scrollTop = chatBox.scrollHeight;
}, delay);
setTimeout(() => { chatBox.scrollTop = chatBox.scrollHeight; }, delay);
});
}
function hideWelcome() {
const w = document.getElementById('welcome');
if (w) w.remove();
}
function addMessage(msgObj, type) {
const area = document.getElementById('messagesArea');
const container = document.createElement('div');
container.className = `msg-container ${type === 'human' ? 'user' : 'bot'}`;
const area = document.getElementById('msgs');
const row = document.createElement('div');
row.className = `msg-row ${type === 'human' ? 'user' : 'bot'}`;
const bubble = document.createElement('div');
bubble.className = `bubble ${type === 'human' ? 'user' : 'bot'}`;
// Handle logic for both API response and History item
let text = "";
let products = [];
if (typeof msgObj === 'string') {
text = msgObj;
} else if (msgObj.message !== undefined) {
// HISTORY ITEM format
text = msgObj.message;
products = msgObj.product_ids || [];
} else {
// API RESPONSE format
text = msgObj.ai_response || "";
products = msgObj.product_ids || [];
}
const txtDiv = document.createElement('div');
txtDiv.textContent = text;
bubble.appendChild(txtDiv);
bubble.textContent = text;
if (products && products.length > 0) {
const row = document.createElement('div');
row.className = 'product-row';
const strip = document.createElement('div');
strip.className = 'product-strip';
products.forEach(p => {
const card = document.createElement('a');
card.className = 'p-card';
// Flexible property mapping for both History (string) and API (object) formats
const isObj = typeof p === 'object' && p !== null;
const name = isObj ? (p.name || p.sku) : (p || 'Sản phẩm Canifa');
const img = isObj ? (p.image || p.thumbnail_image_url) : `https://placehold.co/200x260?text=${p}`;
const price = isObj ? (p.price || p.sale_price || 0) : 0;
const oldPrice = isObj ? ((p.original_price && p.price && p.original_price > p.price) ? p.original_price : null) : null;
const url = isObj ? (p.url ? (p.url.startsWith('http') ? p.url : `https://canifa.com/${p.url}`) : '#') : '#';
const card = document.createElement('a');
card.className = 'p-card';
card.href = url;
card.target = '_blank';
card.innerHTML = `
<div class="p-img-box">
<img src="${img}" class="p-img" onerror="this.src='https://placehold.co/200x260?text=CANIFA'">
<div class="p-img">
<img src="${img}" alt="${name}" loading="lazy" onerror="this.src='https://placehold.co/300x400/111/333?text=CANIFA'">
</div>
<div class="p-info">
<div class="p-meta">
<div class="p-name" title="${name}">${name}</div>
<div class="p-price-row">
<span class="p-price">${price.toLocaleString()}đ</span>
${oldPrice ? `<span class="p-price-old">${oldPrice.toLocaleString()}đ</span>` : ''}
<span class="p-price">${price.toLocaleString('vi-VN')}đ</span>
${oldPrice ? `<span class="p-old">${oldPrice.toLocaleString('vi-VN')}đ</span>` : ''}
</div>
</div>
`;
row.appendChild(card);
</div>`;
strip.appendChild(card);
});
bubble.appendChild(row);
bubble.appendChild(strip);
}
const time = document.createElement('div');
time.className = 'timestamp';
time.className = 'msg-ts';
const ts = msgObj.timestamp ? new Date(msgObj.timestamp) : new Date();
time.textContent = ts.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
time.textContent = ts.toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' });
container.appendChild(bubble);
container.appendChild(time);
area.appendChild(container);
row.appendChild(bubble);
row.appendChild(time);
area.appendChild(row);
scrollToBottom();
// Update stats/debug if it's a fresh response
if (msgObj.response_ready_s) document.getElementById('tLat').textContent = Number(msgObj.response_ready_s).toFixed(2) + 's';
if (msgObj.stage_info) document.getElementById('dStage').textContent = msgObj.stage_info.name;
}
function showTyping() {
const area = document.getElementById('msgs');
const row = document.createElement('div');
row.className = 'msg-row bot'; row.id = 'typing';
const bub = document.createElement('div');
bub.className = 'bubble bot';
bub.innerHTML = '<div class="typing-dots" aria-label="AI is typing"><i></i><i></i><i></i></div>';
row.appendChild(bub);
area.appendChild(row);
scrollToBottom();
}
function hideTyping() {
const t = document.getElementById('typing');
if (t) t.remove();
}
async function sendMessage() {
const input = document.getElementById('userInput');
const query = input.value.trim();
......@@ -399,32 +260,68 @@
if (!query || !devId) return;
hideWelcome();
addMessage(query, 'human');
input.value = '';
showTyping();
try {
const dbSource = document.getElementById('mockMode').checked ? 'sqlite' : 'starrocks';
const data = await CanifaAPI.chat(query, devId, token, dbSource);
hideTyping();
if (data.status === 'success') {
addMessage(data, 'ai');
} else {
addMessage('Lỗi: ' + (data.message || 'Không xác định'), 'ai');
}
} catch (e) {
hideTyping();
addMessage('Lỗi kết nối: ' + e.message, 'ai');
}
}
async function loadPrompt(type) {
function quickSend(text) {
document.getElementById('userInput').value = text;
sendMessage();
}
let currentPromptType = 'system';
async function loadPrompt(type, btnElement = null) {
currentPromptType = type;
if (btnElement) {
document.querySelectorAll('.ptab').forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
btnElement.classList.add('active');
btnElement.setAttribute('aria-selected', 'true');
}
const area = document.getElementById('promptArea');
area.value = 'Đang tải...';
const data = await CanifaAPI.getPrompt(type);
area.value = data.content || '';
try {
const data = await CanifaAPI.getPrompt(type);
area.value = data.content || '';
} catch (e) {
area.value = 'Lỗi tải prompt.';
}
}
async function savePrompt() {
const content = document.getElementById('promptArea').value;
const data = await CanifaAPI.savePrompt(currentPromptType, content);
alert(data.status === 'success' ? 'Cập nhật thành công!' : 'Thất bại!');
const b = document.querySelector('.btn-primary');
const orig = b.textContent;
try {
const data = await CanifaAPI.savePrompt(currentPromptType, content);
if (data.status === 'success') {
b.textContent = 'Đã lưu ✓'; b.style.background = '#2bc270';
} else {
b.textContent = 'Thất bại!'; b.style.background = '#e01830';
}
} catch(e) {
b.textContent = 'Thất bại!'; b.style.background = '#e01830';
}
setTimeout(() => { b.textContent = orig; b.style.background = ''; }, 1600);
}
async function resetChat() {
......@@ -432,30 +329,33 @@
const devId = document.getElementById('deviceId').value;
try {
// 1. Archive on server
await CanifaAPI.archiveHistory(devId);
} catch (e) {
console.error('Server archive failed:', e);
}
// 2. Clear local storage to force new Device ID generation
localStorage.removeItem('canifa_device_id');
// 3. Clear UI and reload
document.getElementById('messagesArea').innerHTML = '';
document.getElementById('msgs').innerHTML = '';
location.reload();
}
window.onload = async () => {
const devId = CanifaAPI.getDeviceId();
let devId = CanifaAPI.getDeviceId();
if (!devId) {
devId = 'DEV-' + Math.random().toString(36).substr(2,8).toUpperCase();
localStorage.setItem('canifa_device_id', devId);
}
document.getElementById('deviceId').value = devId;
loadPrompt('system');
const d = await CanifaAPI.fetchHistory(devId, 15);
const msgs = d.data || [];
msgs.reverse().forEach(m => addMessage(m, m.is_human ? 'human' : 'ai'));
scrollToBottom();
try {
const d = await CanifaAPI.fetchHistory(devId, 15);
const msgs = d.data || [];
if (msgs.length > 0) {
hideWelcome();
msgs.reverse().forEach(m => addMessage(m, m.is_human ? 'human' : 'ai'));
scrollToBottom();
}
} catch(e) {}
};
</script>
</body>
......
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