Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
chatbot canifa
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
chatbot canifa
Commits
33ca1424
Commit
33ca1424
authored
May 11, 2026
by
Vũ Hoàng Anh
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): extract CSS to index.css + premium light-mode redesign
parent
3e2fa498
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
900 additions
and
334 deletions
+900
-334
index.css
backend/static/index.css
+666
-0
index.html
backend/static/index.html
+234
-334
No files found.
backend/static/index.css
0 → 100644
View file @
33ca1424
/* ── 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
:
1
fr
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
:
1
fr
1
fr
;
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
));
}
backend/static/index.html
View file @
33ca1424
<!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
:
100
dvh
;
/* 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
(
'm
essagesArea
'
);
const
container
=
document
.
createElement
(
'div'
);
container
.
className
=
`msg-container
${
type
===
'human'
?
'user'
:
'bot'
}
`
;
const
area
=
document
.
getElementById
(
'm
sgs
'
);
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>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment