| <!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> |