|
|
<!DOCTYPE html> |
|
|
<html lang="ko"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Lamko Chat</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/tokyo-night-dark.min.css"> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
|
|
|
|
|
<style> |
|
|
:root { |
|
|
--bg-primary: #0D0D12; |
|
|
--bg-secondary: #161620; |
|
|
--bg-tertiary: #1E1E2E; |
|
|
--bg-card: #252538; |
|
|
--accent-primary: #6366f1; |
|
|
--accent-secondary: #8b5cf6; |
|
|
--accent-hover: #5b21b6; |
|
|
--text-primary: #f8fafc; |
|
|
--text-secondary: #cbd5e1; |
|
|
--text-muted: #64748b; |
|
|
--border: #334155; |
|
|
--glass-bg: rgba(255, 255, 255, 0.05); |
|
|
--glass-border: rgba(255, 255, 255, 0.1); |
|
|
--shadow-glow: rgba(99, 102, 241, 0.3); |
|
|
--gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6); |
|
|
--gradient-secondary: linear-gradient(135deg, #ec4899, #f59e0b); |
|
|
--gradient-bg: linear-gradient(135deg, #0D0D12 0%, #161620 50%, #1E1E2E 100%); |
|
|
} |
|
|
|
|
|
* { |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif; |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
background: var(--gradient-bg); |
|
|
color: var(--text-primary); |
|
|
height: 100vh; |
|
|
overflow: hidden; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
|
|
|
.animated-bg { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
z-index: -1; |
|
|
background: var(--gradient-bg); |
|
|
} |
|
|
|
|
|
.animated-bg::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
width: 200px; |
|
|
height: 200px; |
|
|
background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%); |
|
|
border-radius: 50%; |
|
|
animation: float1 20s ease-in-out infinite; |
|
|
top: 10%; |
|
|
left: 10%; |
|
|
} |
|
|
|
|
|
.animated-bg::after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
width: 150px; |
|
|
height: 150px; |
|
|
background: radial-gradient(circle, rgba(139, 92, 246, 0.1) 0%, transparent 70%); |
|
|
border-radius: 50%; |
|
|
animation: float2 15s ease-in-out infinite; |
|
|
bottom: 10%; |
|
|
right: 10%; |
|
|
} |
|
|
|
|
|
@keyframes float1 { |
|
|
0%, 100% { transform: translateY(0px) translateX(0px) scale(1); } |
|
|
33% { transform: translateY(-30px) translateX(20px) scale(1.1); } |
|
|
66% { transform: translateY(20px) translateX(-10px) scale(0.9); } |
|
|
} |
|
|
|
|
|
@keyframes float2 { |
|
|
0%, 100% { transform: translateY(0px) translateX(0px) scale(1); } |
|
|
50% { transform: translateY(-20px) translateX(-30px) scale(1.2); } |
|
|
} |
|
|
|
|
|
|
|
|
.app-container { |
|
|
display: flex; |
|
|
height: 100vh; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
|
|
|
.sidebar { |
|
|
width: 320px; |
|
|
background: rgba(22, 22, 32, 0.95); |
|
|
backdrop-filter: blur(20px); |
|
|
border-right: 1px solid var(--glass-border); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
position: relative; |
|
|
z-index: 100; |
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
} |
|
|
|
|
|
.sidebar-header { |
|
|
padding: 2rem; |
|
|
border-bottom: 1px solid var(--glass-border); |
|
|
background: var(--glass-bg); |
|
|
} |
|
|
|
|
|
.logo { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.75rem; |
|
|
margin-bottom: 1.5rem; |
|
|
} |
|
|
|
|
|
.logo-icon { |
|
|
width: 32px; |
|
|
height: 32px; |
|
|
background: var(--gradient-primary); |
|
|
border-radius: 8px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
font-weight: 700; |
|
|
font-size: 1.25rem; |
|
|
} |
|
|
|
|
|
.logo-text { |
|
|
font-size: 1.5rem; |
|
|
font-weight: 700; |
|
|
background: var(--gradient-primary); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
background-clip: text; |
|
|
} |
|
|
|
|
|
.new-chat-btn { |
|
|
width: 100%; |
|
|
padding: 0.875rem 1.25rem; |
|
|
background: var(--gradient-primary); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 12px; |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 0.5rem; |
|
|
box-shadow: 0 4px 15px var(--shadow-glow); |
|
|
} |
|
|
|
|
|
.new-chat-btn:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 8px 25px var(--shadow-glow); |
|
|
} |
|
|
|
|
|
.chat-list { |
|
|
flex: 1; |
|
|
padding: 1.5rem; |
|
|
overflow-y: auto; |
|
|
scrollbar-width: none; |
|
|
-ms-overflow-style: none; |
|
|
} |
|
|
|
|
|
.chat-list::-webkit-scrollbar { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.chat-item { |
|
|
padding: 1rem 1.25rem; |
|
|
margin-bottom: 0.5rem; |
|
|
background: transparent; |
|
|
border: 1px solid transparent; |
|
|
border-radius: 12px; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.chat-item::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background: var(--gradient-primary); |
|
|
opacity: 0; |
|
|
transition: opacity 0.2s; |
|
|
z-index: -1; |
|
|
} |
|
|
|
|
|
.chat-item:hover::before { |
|
|
opacity: 0.1; |
|
|
} |
|
|
|
|
|
.chat-item.active::before { |
|
|
opacity: 0.2; |
|
|
} |
|
|
|
|
|
.chat-item:hover { |
|
|
border-color: var(--accent-primary); |
|
|
transform: translateX(4px); |
|
|
} |
|
|
|
|
|
.chat-title { |
|
|
font-weight: 500; |
|
|
color: var(--text-primary); |
|
|
white-space: nowrap; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
margin-right: 2rem; |
|
|
} |
|
|
|
|
|
.delete-btn { |
|
|
position: absolute; |
|
|
right: 0.75rem; |
|
|
top: 50%; |
|
|
transform: translateY(-50%); |
|
|
background: rgba(239, 68, 68, 0.2); |
|
|
border: none; |
|
|
border-radius: 6px; |
|
|
padding: 0.5rem; |
|
|
color: #ef4444; |
|
|
cursor: pointer; |
|
|
opacity: 0; |
|
|
transition: all 0.2s; |
|
|
width: 28px; |
|
|
height: 28px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.chat-item:hover .delete-btn { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.delete-btn:hover { |
|
|
background: rgba(239, 68, 68, 0.3); |
|
|
transform: translateY(-50%) scale(1.1); |
|
|
} |
|
|
|
|
|
|
|
|
.main-content { |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.chat-header { |
|
|
padding: 1.5rem 2rem; |
|
|
background: var(--glass-bg); |
|
|
backdrop-filter: blur(20px); |
|
|
border-bottom: 1px solid var(--glass-border); |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.chat-title-main { |
|
|
font-size: 1.25rem; |
|
|
font-weight: 600; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.header-actions { |
|
|
display: flex; |
|
|
gap: 0.75rem; |
|
|
} |
|
|
|
|
|
.action-btn { |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
background: var(--glass-bg); |
|
|
border: 1px solid var(--glass-border); |
|
|
border-radius: 12px; |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.action-btn:hover { |
|
|
background: var(--accent-primary); |
|
|
color: white; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3); |
|
|
} |
|
|
|
|
|
|
|
|
.messages-container { |
|
|
flex: 1; |
|
|
overflow-y: auto; |
|
|
padding: 2rem; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 1.5rem; |
|
|
scrollbar-width: thin; |
|
|
scrollbar-color: var(--border) transparent; |
|
|
} |
|
|
|
|
|
.messages-container::-webkit-scrollbar { |
|
|
width: 6px; |
|
|
} |
|
|
|
|
|
.messages-container::-webkit-scrollbar-track { |
|
|
background: transparent; |
|
|
} |
|
|
|
|
|
.messages-container::-webkit-scrollbar-thumb { |
|
|
background: var(--border); |
|
|
border-radius: 3px; |
|
|
} |
|
|
|
|
|
.message { |
|
|
display: flex; |
|
|
gap: 1rem; |
|
|
animation: messageSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
} |
|
|
|
|
|
@keyframes messageSlideIn { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: translateY(20px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
.message.user { |
|
|
flex-direction: row-reverse; |
|
|
} |
|
|
|
|
|
.avatar { |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
border-radius: 12px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-weight: 600; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.avatar.user { |
|
|
background: var(--gradient-secondary); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.avatar.bot { |
|
|
background: var(--gradient-primary); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.message-content { |
|
|
flex: 1; |
|
|
max-width: calc(100% - 60px); |
|
|
} |
|
|
|
|
|
.message-bubble { |
|
|
padding: 1.25rem 1.5rem; |
|
|
border-radius: 20px; |
|
|
position: relative; |
|
|
word-wrap: break-word; |
|
|
} |
|
|
|
|
|
.message.user .message-bubble { |
|
|
background: var(--gradient-secondary); |
|
|
color: white; |
|
|
border-bottom-right-radius: 6px; |
|
|
margin-left: 2rem; |
|
|
} |
|
|
|
|
|
.message.bot .message-bubble { |
|
|
background: var(--bg-card); |
|
|
border: 1px solid var(--glass-border); |
|
|
color: var(--text-primary); |
|
|
border-bottom-left-radius: 6px; |
|
|
margin-right: 2rem; |
|
|
} |
|
|
|
|
|
.message-time { |
|
|
font-size: 0.75rem; |
|
|
color: var(--text-muted); |
|
|
margin-top: 0.5rem; |
|
|
padding-left: 0.5rem; |
|
|
} |
|
|
|
|
|
.message.user .message-time { |
|
|
text-align: right; |
|
|
padding-right: 0.5rem; |
|
|
padding-left: 0; |
|
|
} |
|
|
|
|
|
|
|
|
.input-area { |
|
|
padding: 2rem; |
|
|
background: var(--glass-bg); |
|
|
backdrop-filter: blur(20px); |
|
|
border-top: 1px solid var(--glass-border); |
|
|
} |
|
|
|
|
|
.input-container { |
|
|
position: relative; |
|
|
max-width: 1000px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
.input-wrapper { |
|
|
display: flex; |
|
|
align-items: flex-end; |
|
|
gap: 1rem; |
|
|
background: var(--bg-card); |
|
|
border: 2px solid var(--glass-border); |
|
|
border-radius: 20px; |
|
|
padding: 1rem 1.5rem; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.input-wrapper:focus-within { |
|
|
border-color: var(--accent-primary); |
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); |
|
|
} |
|
|
|
|
|
.message-input { |
|
|
flex: 1; |
|
|
background: transparent; |
|
|
border: none; |
|
|
outline: none; |
|
|
color: var(--text-primary); |
|
|
font-size: 1rem; |
|
|
resize: none; |
|
|
min-height: 24px; |
|
|
max-height: 120px; |
|
|
font-family: inherit; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.message-input::placeholder { |
|
|
color: var(--text-muted); |
|
|
} |
|
|
|
|
|
.send-btn { |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
background: var(--gradient-primary); |
|
|
border: none; |
|
|
border-radius: 12px; |
|
|
color: white; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.send-btn:hover:not(:disabled) { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 15px var(--shadow-glow); |
|
|
} |
|
|
|
|
|
.send-btn:disabled { |
|
|
opacity: 0.5; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
|
|
|
.typing-indicator { |
|
|
display: flex; |
|
|
gap: 1rem; |
|
|
padding: 1rem 0; |
|
|
} |
|
|
|
|
|
.typing-avatar { |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
background: var(--gradient-primary); |
|
|
border-radius: 12px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.typing-bubble { |
|
|
background: var(--bg-card); |
|
|
border: 1px solid var(--glass-border); |
|
|
border-radius: 20px; |
|
|
border-bottom-left-radius: 6px; |
|
|
padding: 1.25rem 1.5rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.typing-dots { |
|
|
display: flex; |
|
|
gap: 0.25rem; |
|
|
} |
|
|
|
|
|
.typing-dot { |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
background: var(--accent-primary); |
|
|
border-radius: 50%; |
|
|
animation: typingPulse 1.4s infinite; |
|
|
} |
|
|
|
|
|
.typing-dot:nth-child(2) { |
|
|
animation-delay: 0.2s; |
|
|
} |
|
|
|
|
|
.typing-dot:nth-child(3) { |
|
|
animation-delay: 0.4s; |
|
|
} |
|
|
|
|
|
@keyframes typingPulse { |
|
|
0%, 60%, 100% { |
|
|
opacity: 0.3; |
|
|
transform: scale(1); |
|
|
} |
|
|
30% { |
|
|
opacity: 1; |
|
|
transform: scale(1.2); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.welcome-screen { |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
text-align: center; |
|
|
padding: 2rem; |
|
|
} |
|
|
|
|
|
.welcome-icon { |
|
|
width: 80px; |
|
|
height: 80px; |
|
|
background: var(--gradient-primary); |
|
|
border-radius: 20px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
margin-bottom: 2rem; |
|
|
box-shadow: 0 10px 30px var(--shadow-glow); |
|
|
} |
|
|
|
|
|
.welcome-title { |
|
|
font-size: 2rem; |
|
|
font-weight: 700; |
|
|
margin-bottom: 1rem; |
|
|
background: var(--gradient-primary); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
background-clip: text; |
|
|
} |
|
|
|
|
|
.welcome-subtitle { |
|
|
font-size: 1.125rem; |
|
|
color: var(--text-secondary); |
|
|
margin-bottom: 2rem; |
|
|
} |
|
|
|
|
|
.welcome-features { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
|
|
gap: 1rem; |
|
|
max-width: 600px; |
|
|
} |
|
|
|
|
|
.feature-card { |
|
|
background: var(--glass-bg); |
|
|
border: 1px solid var(--glass-border); |
|
|
border-radius: 16px; |
|
|
padding: 1.5rem; |
|
|
text-align: left; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.feature-card:hover { |
|
|
background: rgba(99, 102, 241, 0.1); |
|
|
border-color: var(--accent-primary); |
|
|
transform: translateY(-4px); |
|
|
} |
|
|
|
|
|
.feature-icon { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
background: var(--gradient-primary); |
|
|
border-radius: 10px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
margin-bottom: 1rem; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.feature-title { |
|
|
font-weight: 600; |
|
|
margin-bottom: 0.5rem; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.feature-desc { |
|
|
font-size: 0.875rem; |
|
|
color: var(--text-muted); |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.sidebar { |
|
|
position: fixed; |
|
|
z-index: 200; |
|
|
height: 100vh; |
|
|
transform: translateX(-100%); |
|
|
} |
|
|
|
|
|
.sidebar.active { |
|
|
transform: translateX(0); |
|
|
} |
|
|
|
|
|
.mobile-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background: rgba(0, 0, 0, 0.5); |
|
|
z-index: 150; |
|
|
opacity: 0; |
|
|
visibility: hidden; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
.mobile-overlay.active { |
|
|
opacity: 1; |
|
|
visibility: visible; |
|
|
} |
|
|
|
|
|
.main-content { |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.chat-header { |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.messages-container { |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.message-bubble { |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.message.user .message-bubble { |
|
|
margin-left: 1rem; |
|
|
} |
|
|
|
|
|
.message.bot .message-bubble { |
|
|
margin-right: 1rem; |
|
|
} |
|
|
|
|
|
.input-area { |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.welcome-features { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.toast { |
|
|
position: fixed; |
|
|
top: 2rem; |
|
|
right: 2rem; |
|
|
background: var(--bg-card); |
|
|
border: 1px solid var(--glass-border); |
|
|
border-radius: 12px; |
|
|
padding: 1rem 1.5rem; |
|
|
color: var(--text-primary); |
|
|
transform: translateX(100%); |
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
z-index: 1000; |
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.toast.show { |
|
|
transform: translateX(0); |
|
|
} |
|
|
|
|
|
.toast.success { |
|
|
border-color: #10b981; |
|
|
background: rgba(16, 185, 129, 0.1); |
|
|
} |
|
|
|
|
|
.toast.error { |
|
|
border-color: #ef4444; |
|
|
background: rgba(239, 68, 68, 0.1); |
|
|
} |
|
|
|
|
|
|
|
|
.message-bubble pre { |
|
|
background: var(--bg-primary); |
|
|
border: 1px solid var(--glass-border); |
|
|
border-radius: 12px; |
|
|
padding: 1rem; |
|
|
margin: 1rem 0; |
|
|
overflow-x: auto; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.code-copy-btn { |
|
|
position: absolute; |
|
|
top: 0.75rem; |
|
|
right: 0.75rem; |
|
|
background: var(--accent-primary); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 6px; |
|
|
padding: 0.375rem 0.75rem; |
|
|
font-size: 0.75rem; |
|
|
cursor: pointer; |
|
|
opacity: 0; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.message-bubble pre:hover .code-copy-btn { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.code-copy-btn:hover { |
|
|
background: var(--accent-hover); |
|
|
transform: scale(1.05); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="animated-bg"></div> |
|
|
|
|
|
<div class="app-container"> |
|
|
|
|
|
<div class="sidebar" id="sidebar"> |
|
|
<div class="sidebar-header"> |
|
|
<div class="logo"> |
|
|
<div class="logo-icon">L</div> |
|
|
<div class="logo-text">Lamko</div> |
|
|
</div> |
|
|
<button class="new-chat-btn" id="newChatBtn"> |
|
|
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/> |
|
|
</svg> |
|
|
์ ๋ํ |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="chat-list" id="chatList"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="main-content"> |
|
|
|
|
|
<div class="chat-header"> |
|
|
<div class="chat-title-main" id="currentChatTitle">Lamko Chat</div> |
|
|
<div class="header-actions"> |
|
|
<button class="action-btn" id="menuBtn"> |
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/> |
|
|
</svg> |
|
|
</button> |
|
|
<button class="action-btn" id="clearBtn"> |
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> |
|
|
</svg> |
|
|
</button> |
|
|
<button class="action-btn" id="downloadBtn"> |
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> |
|
|
</svg> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="messages-container" id="messagesContainer"> |
|
|
<div class="welcome-screen" id="welcomeScreen"> |
|
|
<div class="welcome-icon"> |
|
|
<svg width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> |
|
|
</svg> |
|
|
</div> |
|
|
<h1 class="welcome-title">Lamko์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค</h1> |
|
|
<p class="welcome-subtitle">AI์ ๋ํ๋ฅผ ์์ํด๋ณด์ธ์. ๊ถ๊ธํ ๊ฒ์ด ์๋ค๋ฉด ์ธ์ ๋ ๋ฌผ์ด๋ณด์ธ์!</p> |
|
|
|
|
|
<div class="welcome-features"> |
|
|
<div class="feature-card"> |
|
|
<div class="feature-icon"> |
|
|
|
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="feature-title">๋น ๋ฅธ ์๋ต</div> |
|
|
<div class="feature-desc">์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ์ผ๋ก ์ฆ๊ฐ์ ์ธ ๋ต๋ณ์ ๋ฐ์๋ณด์ธ์</div> |
|
|
</div> |
|
|
|
|
|
<div class="feature-card"> |
|
|
<div class="feature-icon"> |
|
|
|
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<circle cx="11" cy="11" r="8" stroke-width="2"/> |
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65" stroke-width="2"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="feature-title">๊ฒ์</div> |
|
|
<div class="feature-desc">์ธ๋ถ์์ ์ ๋ณด๋ฅผ ๊ฒ์ํฉ๋๋ค. '๊ฒ์: ๊ถ๊ธํ ๋ด์ฉ' ํ์์ผ๋ก ์ฌ์ฉํฉ๋๋ค.</div> |
|
|
</div> |
|
|
|
|
|
<div class="feature-card"> |
|
|
<div class="feature-icon"> |
|
|
|
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 2v20m-6-6h12"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="feature-title">๋ํ ์ ์ฅ</div> |
|
|
<div class="feature-desc">๋ชจ๋ ๋ํ๋ ์๋์ผ๋ก ์ ์ฅ๋์ด ์ธ์ ๋ ๋ค์ ํ์ธํ ์ ์์ต๋๋ค</div> |
|
|
</div> |
|
|
|
|
|
<div class="feature-card"> |
|
|
<div class="feature-icon"> |
|
|
|
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<circle cx="12" cy="12" r="10" stroke-width="2"/> |
|
|
<circle cx="12" cy="12" r="5" stroke-width="2"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="feature-title">๋ค์ค ๋ชจ๋ธ</div> |
|
|
<div class="feature-desc">๊ฐ ์ํฉ์ ์ต์ ํ๋ ์ฌ๋ฌ ๋ชจ๋ธ์ ์ฌ์ฉํฉ๋๋ค</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="input-area"> |
|
|
<div class="input-container"> |
|
|
<div class="input-wrapper"> |
|
|
<textarea |
|
|
class="message-input" |
|
|
id="messageInput" |
|
|
placeholder="๋ฉ์์ง๋ฅผ ์
๋ ฅํ์ธ์..." |
|
|
rows="1" |
|
|
></textarea> |
|
|
<button class="send-btn" id="sendBtn"> |
|
|
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/> |
|
|
</svg> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mobile-overlay" id="mobileOverlay"></div> |
|
|
|
|
|
|
|
|
<div class="toast" id="toast"></div> |
|
|
|
|
|
<script> |
|
|
class NexusChat { |
|
|
constructor() { |
|
|
this.STORAGE_KEY = 'nexus_chatrooms'; |
|
|
this.chatrooms = this.loadChatrooms(); |
|
|
this.currentRoom = null; |
|
|
this.isLoading = false; |
|
|
this.eventSource = null; |
|
|
|
|
|
this.initializeElements(); |
|
|
this.setupEventListeners(); |
|
|
this.renderChatList(); |
|
|
this.showWelcome(); |
|
|
|
|
|
this.debouncedSave = this.debounce(this.saveChatrooms.bind(this), 500); |
|
|
} |
|
|
|
|
|
debounce(func, delay) { |
|
|
let timeoutId; |
|
|
return (...args) => { |
|
|
clearTimeout(timeoutId); |
|
|
timeoutId = setTimeout(() => func.apply(this, args), delay); |
|
|
}; |
|
|
} |
|
|
|
|
|
generateRoomName(text) { |
|
|
return text.length > 40 ? text.substring(0, 40) + '...' : text; |
|
|
} |
|
|
|
|
|
formatTime(timestamp) { |
|
|
return new Date(timestamp).toLocaleTimeString('ko-KR', { |
|
|
hour: '2-digit', |
|
|
minute: '2-digit' |
|
|
}); |
|
|
} |
|
|
|
|
|
showToast(message, type = 'info') { |
|
|
const toast = document.getElementById('toast'); |
|
|
toast.textContent = message; |
|
|
toast.className = `toast show ${type}`; |
|
|
setTimeout(() => toast.classList.remove('show'), 3000); |
|
|
} |
|
|
|
|
|
loadChatrooms() { |
|
|
try { |
|
|
return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '{}'); |
|
|
} catch (error) { |
|
|
console.error('์ฑํ
๋ฐฉ ๋ฐ์ดํฐ ๋ก๋ ์คํจ:', error); |
|
|
return {}; |
|
|
} |
|
|
} |
|
|
|
|
|
saveChatrooms() { |
|
|
try { |
|
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.chatrooms)); |
|
|
} catch (error) { |
|
|
console.error('์ฑํ
๋ฐฉ ๋ฐ์ดํฐ ์ ์ฅ ์คํจ:', error); |
|
|
this.showToast('์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
initializeElements() { |
|
|
this.elements = { |
|
|
sidebar: document.getElementById('sidebar'), |
|
|
chatList: document.getElementById('chatList'), |
|
|
messagesContainer: document.getElementById('messagesContainer'), |
|
|
welcomeScreen: document.getElementById('welcomeScreen'), |
|
|
messageInput: document.getElementById('messageInput'), |
|
|
sendBtn: document.getElementById('sendBtn'), |
|
|
currentChatTitle: document.getElementById('currentChatTitle'), |
|
|
mobileOverlay: document.getElementById('mobileOverlay') |
|
|
}; |
|
|
} |
|
|
|
|
|
setupEventListeners() { |
|
|
this.elements.messageInput.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
this.sendMessage(); |
|
|
} |
|
|
}); |
|
|
|
|
|
this.elements.messageInput.addEventListener('input', () => this.adjustTextareaHeight()); |
|
|
|
|
|
this.elements.sendBtn.addEventListener('click', () => this.sendMessage()); |
|
|
|
|
|
document.getElementById('menuBtn').addEventListener('click', () => this.toggleSidebar()); |
|
|
document.getElementById('clearBtn').addEventListener('click', () => this.clearChat()); |
|
|
document.getElementById('downloadBtn').addEventListener('click', () => this.downloadChat()); |
|
|
document.getElementById('newChatBtn').addEventListener('click', () => this.createNewChat()); |
|
|
|
|
|
this.elements.mobileOverlay.addEventListener('click', () => this.closeSidebar()); |
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Escape') this.closeSidebar(); |
|
|
}); |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
|
if (window.innerWidth > 768) this.closeSidebar(); |
|
|
}); |
|
|
} |
|
|
|
|
|
adjustTextareaHeight() { |
|
|
const textarea = this.elements.messageInput; |
|
|
textarea.style.height = 'auto'; |
|
|
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; |
|
|
} |
|
|
|
|
|
toggleSidebar() { |
|
|
if (window.innerWidth <= 768) { |
|
|
this.elements.sidebar.classList.toggle('active'); |
|
|
this.elements.mobileOverlay.classList.toggle('active'); |
|
|
document.body.style.overflow = this.elements.sidebar.classList.contains('active') ? 'hidden' : ''; |
|
|
} |
|
|
} |
|
|
|
|
|
showWelcome() { |
|
|
this.elements.welcomeScreen.style.display = 'flex'; |
|
|
this.elements.currentChatTitle.textContent = 'Lamko Chat'; |
|
|
} |
|
|
|
|
|
hideWelcome() { |
|
|
this.elements.welcomeScreen.style.display = 'none'; |
|
|
} |
|
|
|
|
|
createNewChat() { |
|
|
const name = prompt('์ ์ฑํ
๋ฐฉ ์ด๋ฆ์ ์
๋ ฅํ์ธ์:'); |
|
|
if (!name || !name.trim()) return; |
|
|
const roomName = name.trim(); |
|
|
if (this.chatrooms[roomName]) return this.showToast('์ด๋ฏธ ์กด์ฌํ๋ ์ฑํ
๋ฐฉ์
๋๋ค.', 'error'); |
|
|
this.addChatRoom(roomName); |
|
|
this.closeSidebar(); |
|
|
} |
|
|
|
|
|
addChatRoom(name) { |
|
|
this.chatrooms[name] = []; |
|
|
this.currentRoom = name; |
|
|
this.saveChatrooms(); |
|
|
this.renderChatList(); |
|
|
this.renderMessages(); |
|
|
this.updateTitle(); |
|
|
} |
|
|
|
|
|
deleteChatRoom(name) { |
|
|
if (!confirm(`'${name}' ์ฑํ
๋ฐฉ์ ์ญ์ ํ์๊ฒ ์ต๋๊น?`)) return; |
|
|
delete this.chatrooms[name]; |
|
|
if (this.currentRoom === name) this.currentRoom = Object.keys(this.chatrooms)[0] || null; |
|
|
this.saveChatrooms(); |
|
|
this.renderChatList(); |
|
|
this.renderMessages(); |
|
|
this.updateTitle(); |
|
|
} |
|
|
|
|
|
selectChatRoom(name) { |
|
|
if (this.isLoading) return; |
|
|
this.currentRoom = name; |
|
|
this.renderChatList(); |
|
|
this.renderMessages(); |
|
|
this.updateTitle(); |
|
|
this.closeSidebar(); |
|
|
} |
|
|
|
|
|
updateTitle() { |
|
|
this.elements.currentChatTitle.textContent = this.currentRoom || 'Lamko Chat'; |
|
|
} |
|
|
|
|
|
renderChatList() { |
|
|
this.elements.chatList.innerHTML = ''; |
|
|
Object.keys(this.chatrooms).forEach(roomName => { |
|
|
const chatItem = document.createElement('div'); |
|
|
chatItem.className = 'chat-item' + (roomName === this.currentRoom ? ' active' : ''); |
|
|
|
|
|
const chatTitle = document.createElement('div'); |
|
|
chatTitle.className = 'chat-title'; |
|
|
chatTitle.textContent = roomName; |
|
|
|
|
|
const deleteBtn = document.createElement('button'); |
|
|
deleteBtn.className = 'delete-btn'; |
|
|
deleteBtn.innerHTML = 'ร'; |
|
|
deleteBtn.onclick = (e) => { |
|
|
e.stopPropagation(); |
|
|
this.deleteChatRoom(roomName); |
|
|
}; |
|
|
|
|
|
chatItem.appendChild(chatTitle); |
|
|
chatItem.appendChild(deleteBtn); |
|
|
chatItem.onclick = () => this.selectChatRoom(roomName); |
|
|
|
|
|
this.elements.chatList.appendChild(chatItem); |
|
|
}); |
|
|
} |
|
|
|
|
|
renderMessages() { |
|
|
this.elements.messagesContainer.innerHTML = ''; |
|
|
if (!this.currentRoom) return this.showWelcome(); |
|
|
this.hideWelcome(); |
|
|
const messages = this.chatrooms[this.currentRoom] || []; |
|
|
messages.forEach(msg => this.renderMessage(msg, false)); |
|
|
this.scrollToBottom(); |
|
|
} |
|
|
|
|
|
renderMessage(msg, animate = true) { |
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.className = 'message ' + msg.role; |
|
|
|
|
|
const avatar = document.createElement('div'); |
|
|
avatar.className = 'avatar ' + msg.role; |
|
|
avatar.textContent = msg.role === 'user' ? 'U' : 'AI'; |
|
|
|
|
|
const messageContent = document.createElement('div'); |
|
|
messageContent.className = 'message-content'; |
|
|
|
|
|
const messageBubble = document.createElement('div'); |
|
|
messageBubble.className = 'message-bubble'; |
|
|
try { messageBubble.innerHTML = marked.parse(msg.text || ''); } |
|
|
catch { messageBubble.textContent = msg.text || ''; } |
|
|
|
|
|
const messageTime = document.createElement('div'); |
|
|
messageTime.className = 'message-time'; |
|
|
messageTime.textContent = this.formatTime(msg.ts); |
|
|
|
|
|
messageContent.appendChild(messageBubble); |
|
|
messageContent.appendChild(messageTime); |
|
|
messageDiv.appendChild(avatar); |
|
|
messageDiv.appendChild(messageContent); |
|
|
this.elements.messagesContainer.appendChild(messageDiv); |
|
|
|
|
|
this.enhanceCodeBlocks(messageDiv); |
|
|
if (animate) this.scrollToBottom(); |
|
|
} |
|
|
|
|
|
enhanceCodeBlocks(messageDiv) { |
|
|
messageDiv.querySelectorAll('pre > code').forEach(code => { |
|
|
try { hljs.highlightElement(code); } catch {} |
|
|
const pre = code.parentElement; |
|
|
if (!pre.querySelector('.code-copy-btn')) { |
|
|
const copyBtn = document.createElement('button'); |
|
|
copyBtn.className = 'code-copy-btn'; |
|
|
copyBtn.textContent = '๋ณต์ฌ'; |
|
|
copyBtn.onclick = async () => { |
|
|
try { await navigator.clipboard.writeText(code.innerText); copyBtn.textContent='๋ณต์ฌ๋จ!'; setTimeout(()=>copyBtn.textContent='๋ณต์ฌ',1500);} |
|
|
catch {copyBtn.textContent='์คํจ'; setTimeout(()=>copyBtn.textContent='๋ณต์ฌ',1500);} |
|
|
}; |
|
|
pre.appendChild(copyBtn); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
scrollToBottom() { |
|
|
requestAnimationFrame(() => { |
|
|
this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight; |
|
|
}); |
|
|
} |
|
|
|
|
|
showTypingIndicator() { |
|
|
const typingDiv = document.createElement('div'); |
|
|
typingDiv.className = 'typing-indicator'; |
|
|
typingDiv.id = 'typing-indicator'; |
|
|
typingDiv.innerHTML = ` |
|
|
<div class="typing-avatar">AI</div> |
|
|
<div class="typing-bubble"> |
|
|
<span>AI๊ฐ ์
๋ ฅ ์ค</span> |
|
|
<div class="typing-dots"> |
|
|
<div class="typing-dot"></div> |
|
|
<div class="typing-dot"></div> |
|
|
<div class="typing-dot"></div> |
|
|
</div> |
|
|
</div>`; |
|
|
this.elements.messagesContainer.appendChild(typingDiv); |
|
|
this.scrollToBottom(); |
|
|
} |
|
|
|
|
|
hideTypingIndicator() { |
|
|
const indicator = document.getElementById('typing-indicator'); |
|
|
if (indicator) indicator.remove(); |
|
|
} |
|
|
|
|
|
async sendMessage() { |
|
|
const text = this.elements.messageInput.value.trim(); |
|
|
if (!text || this.isLoading) return; |
|
|
|
|
|
this.elements.messageInput.value = ''; |
|
|
this.adjustTextareaHeight(); |
|
|
this.isLoading = true; |
|
|
this.elements.sendBtn.disabled = true; |
|
|
|
|
|
if (!this.currentRoom) { |
|
|
const roomName = this.generateRoomName(text); |
|
|
this.addChatRoom(roomName); |
|
|
} |
|
|
|
|
|
const userMsg = { role: 'user', text, ts: Date.now() }; |
|
|
this.chatrooms[this.currentRoom].push(userMsg); |
|
|
this.debouncedSave(); |
|
|
this.renderMessage(userMsg); |
|
|
|
|
|
|
|
|
if (text.startsWith('๊ฒ์:')) { |
|
|
const query = text.replace(/^๊ฒ์:\s*/, ''); |
|
|
await this.performSearch(query); |
|
|
} else { |
|
|
this.showTypingIndicator(); |
|
|
const botMsg = { role: 'bot', text: '', ts: Date.now() + 1 }; |
|
|
this.chatrooms[this.currentRoom].push(botMsg); |
|
|
this.debouncedSave(); |
|
|
try { await this.streamBotResponse(text, botMsg); } |
|
|
catch (e) { botMsg.text='โ ๏ธ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.'; this.debouncedSave(); } |
|
|
finally { this.hideTypingIndicator(); this.isLoading=false; this.elements.sendBtn.disabled=false; this.elements.messageInput.focus(); } |
|
|
} |
|
|
} |
|
|
|
|
|
async performSearch(query) { |
|
|
const botMsg = { role: 'bot', text: '', ts: Date.now() + 1 }; |
|
|
this.chatrooms[this.currentRoom].push(botMsg); |
|
|
this.debouncedSave(); |
|
|
this.renderMessage(botMsg); |
|
|
|
|
|
try { |
|
|
const res = await fetch(`/api/search?query=${encodeURIComponent(query)}`); |
|
|
const data = await res.json(); |
|
|
let formatted = `### ๊ฒ์ ๊ฒฐ๊ณผ: ${query}\n`; |
|
|
data.results.forEach((item, idx) => { |
|
|
formatted += `**${idx+1}. [${item.title}](${item.link})**\n\n${item.snippet}\n\n`; |
|
|
}); |
|
|
botMsg.text = formatted; |
|
|
const lastMessage = Array.from(this.elements.messagesContainer.querySelectorAll('.message.bot')).pop(); |
|
|
this.updateBotMessage(lastMessage.querySelector('.message-bubble'), formatted, lastMessage); |
|
|
} catch (error) { |
|
|
botMsg.text='โ ๏ธ ๊ฒ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.'; |
|
|
const lastMessage = Array.from(this.elements.messagesContainer.querySelectorAll('.message.bot')).pop(); |
|
|
lastMessage.querySelector('.message-bubble').textContent = botMsg.text; |
|
|
} finally { |
|
|
this.isLoading=false; |
|
|
this.elements.sendBtn.disabled=false; |
|
|
this.elements.messageInput.focus(); |
|
|
} |
|
|
} |
|
|
|
|
|
async streamBotResponse(text, botMsg) { |
|
|
return new Promise((resolve, reject) => { |
|
|
this.hideTypingIndicator(); |
|
|
this.renderMessage(botMsg); |
|
|
|
|
|
const lastMessage = Array.from(this.elements.messagesContainer.querySelectorAll('.message.bot')).pop(); |
|
|
const contentNode = lastMessage.querySelector('.message-bubble'); |
|
|
|
|
|
const url = `/api/chat?message=${encodeURIComponent(text)}`; |
|
|
|
|
|
if (this.eventSource) { |
|
|
this.eventSource.close(); |
|
|
} |
|
|
|
|
|
this.eventSource = new EventSource(url); |
|
|
let fullResponse = ''; |
|
|
let lastUpdateTime = Date.now(); |
|
|
|
|
|
this.eventSource.onmessage = (event) => { |
|
|
try { |
|
|
const data = JSON.parse(event.data); |
|
|
|
|
|
if (data.char !== undefined) { |
|
|
fullResponse += data.char; |
|
|
botMsg.text = fullResponse; |
|
|
|
|
|
const now = Date.now(); |
|
|
if (now - lastUpdateTime > 50) { |
|
|
this.updateBotMessage(contentNode, fullResponse, lastMessage); |
|
|
lastUpdateTime = now; |
|
|
} |
|
|
} else if (data.done) { |
|
|
this.updateBotMessage(contentNode, fullResponse, lastMessage); |
|
|
this.eventSource.close(); |
|
|
this.eventSource = null; |
|
|
resolve(); |
|
|
} else if (data.error) { |
|
|
this.eventSource.close(); |
|
|
this.eventSource = null; |
|
|
botMsg.text = 'โ ๏ธ ' + data.error; |
|
|
contentNode.textContent = botMsg.text; |
|
|
reject(new Error(data.error)); |
|
|
} |
|
|
} catch (parseError) { |
|
|
fullResponse += event.data; |
|
|
botMsg.text = fullResponse; |
|
|
contentNode.textContent = fullResponse; |
|
|
} |
|
|
|
|
|
this.debouncedSave(); |
|
|
}; |
|
|
|
|
|
this.eventSource.onerror = () => { |
|
|
this.eventSource.close(); |
|
|
this.eventSource = null; |
|
|
|
|
|
if (!fullResponse) { |
|
|
botMsg.text = 'โ ๏ธ ์ฐ๊ฒฐ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.'; |
|
|
contentNode.textContent = botMsg.text; |
|
|
reject(new Error('Connection error')); |
|
|
} else { |
|
|
resolve(); |
|
|
} |
|
|
}; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
updateBotMessage(bubbleEl, text, messageDiv) { |
|
|
try { bubbleEl.innerHTML = marked.parse(text); } catch { bubbleEl.textContent=text; } |
|
|
this.enhanceCodeBlocks(messageDiv); |
|
|
this.scrollToBottom(); |
|
|
} |
|
|
|
|
|
clearChat() { |
|
|
if (!this.currentRoom) return; |
|
|
if (!confirm('์ฑํ
๊ธฐ๋ก์ ์ญ์ ํ์๊ฒ ์ต๋๊น?')) return; |
|
|
this.chatrooms[this.currentRoom] = []; |
|
|
this.saveChatrooms(); |
|
|
this.renderMessages(); |
|
|
} |
|
|
|
|
|
downloadChat() { |
|
|
if (!this.currentRoom) return; |
|
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(this.chatrooms[this.currentRoom], null, 2)); |
|
|
const dlAnchor = document.createElement('a'); |
|
|
dlAnchor.setAttribute("href", dataStr); |
|
|
dlAnchor.setAttribute("download", `${this.currentRoom}.json`); |
|
|
dlAnchor.click(); |
|
|
} |
|
|
|
|
|
closeSidebar() { |
|
|
if (window.innerWidth <= 768) { |
|
|
this.elements.sidebar.classList.remove('active'); |
|
|
this.elements.mobileOverlay.classList.remove('active'); |
|
|
document.body.style.overflow = ''; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => new NexusChat()); |
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', () => { |
|
|
if (window.nexusChat) { |
|
|
window.nexusChat.cleanup(); |
|
|
} |
|
|
}); |
|
|
|
|
|
window.addEventListener('error', (e) => { |
|
|
console.error('์ ์ญ ์ค๋ฅ:', e.error); |
|
|
}); |
|
|
|
|
|
window.addEventListener('unhandledrejection', (e) => { |
|
|
console.error('์ฒ๋ฆฌ๋์ง ์์ Promise ๊ฑฐ๋ถ:', e.reason); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |