LamCo / static /index.html
AlphaCNN's picture
Rename static/index(1).html to static/index.html
b661363 verified
<!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๊ฐ€ ๋ˆ„๋ฝ๋˜์–ด ์žˆ์—ˆ์Œ -->
<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>