Download static/index.html from AlphaCNN/LamCo: direct link, hf CLI and curl.
- Browser
- Download file 35.8 kB
-
https://huggingface.co/spaces/AlphaCNN/LamCo/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/AlphaCNN/LamCo/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/AlphaCNN/LamCo/resolve/main/static/index.html
35.8 kB
| <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> |