Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Krish Mind AI</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| :root { | |
| /* Light Theme */ | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f7f7f8; | |
| --bg-tertiary: #ececf1; | |
| --bg-sidebar: #f9f9f9; | |
| --bg-hover: #f0f0f5; | |
| --bg-input: #ffffff; | |
| --text-primary: #1a1a1a; | |
| --text-secondary: #5d5d5d; | |
| --text-muted: #8e8ea0; | |
| --border: #e5e5e5; | |
| --border-light: #f0f0f0; | |
| --accent: #0066cc; | |
| --accent-gradient: linear-gradient(135deg, #1e3a5f 0%, #2d8cbe 50%, #40c9c9 100%); | |
| --user-bg: #f7f7f8; | |
| --assistant-bg: #ffffff; | |
| --code-bg: #1e1e1e; | |
| --shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | |
| --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15); | |
| } | |
| [data-theme="dark"] { | |
| --bg-primary: #212121; | |
| --bg-secondary: #2f2f2f; | |
| --bg-tertiary: #3a3a3a; | |
| --bg-sidebar: #171717; | |
| --bg-hover: #3a3a3a; | |
| --bg-input: #2f2f2f; | |
| --text-primary: #ececec; | |
| --text-secondary: #b4b4b4; | |
| --text-muted: #8e8ea0; | |
| --border: #3a3a3a; | |
| --border-light: #2f2f2f; | |
| --accent: #40c9c9; | |
| --user-bg: #2f2f2f; | |
| --assistant-bg: #212121; | |
| --code-bg: #0d0d0d; | |
| --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); | |
| --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| display: flex; | |
| transition: background 0.3s, color 0.3s; | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| width: 260px; | |
| min-width: 260px; | |
| background: var(--bg-sidebar); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| transition: all 0.3s ease; | |
| flex-shrink: 0; | |
| } | |
| /* Hidden state - collapses sidebar on desktop */ | |
| .sidebar.hidden { | |
| width: 0; | |
| min-width: 0; | |
| border-right: none; | |
| overflow: hidden; | |
| } | |
| .sidebar-header { | |
| padding: 12px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .new-chat-btn { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| transition: background 0.2s; | |
| } | |
| .new-chat-btn:hover { | |
| background: var(--bg-hover); | |
| } | |
| .chat-history { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 8px; | |
| } | |
| .chat-history-item { | |
| padding: 10px 12px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| transition: background 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .chat-history-item:hover { | |
| background: var(--bg-hover); | |
| } | |
| .chat-history-item.active { | |
| background: var(--bg-tertiary); | |
| } | |
| .sidebar-footer { | |
| padding: 12px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .connection-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 12px; | |
| background: var(--bg-secondary); | |
| border-radius: 8px; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #ef4444; | |
| } | |
| .status-dot.online { | |
| background: #22c55e; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); | |
| } | |
| 50% { | |
| box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); | |
| } | |
| } | |
| /* Main Content */ | |
| .main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| min-width: 0; | |
| } | |
| /* Header */ | |
| .header { | |
| padding: 12px 20px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: var(--bg-primary); | |
| } | |
| .logo-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo-img { | |
| height: 32px; | |
| } | |
| .model-selector { | |
| padding: 8px 12px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 13px; | |
| cursor: pointer; | |
| } | |
| .header-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .icon-btn { | |
| width: 36px; | |
| height: 36px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: transparent; | |
| border: none; | |
| border-radius: 8px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 18px; | |
| transition: background 0.2s, color 0.2s; | |
| } | |
| .icon-btn:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| /* Chat Container */ | |
| .chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0; | |
| } | |
| .chat-messages { | |
| max-width: 768px; | |
| margin: 0 auto; | |
| padding: 24px; | |
| } | |
| /* Messages */ | |
| .message { | |
| margin-bottom: 24px; | |
| animation: fadeIn 0.3s ease; | |
| display: flex; | |
| gap: 12px; | |
| max-width: 85%; | |
| } | |
| .message.user { | |
| margin-left: auto; | |
| flex-direction: row-reverse; | |
| } | |
| .message.assistant { | |
| margin-right: auto; | |
| flex-direction: row; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(8px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Removed .message-header styles as we are restructuring */ | |
| /* Avatar styles removed */ | |
| .message-role { | |
| display: none; | |
| /* Hide names */ | |
| } | |
| .message-content { | |
| padding: 12px 16px; | |
| border-radius: 16px; | |
| line-height: 1.6; | |
| color: var(--text-primary); | |
| overflow-wrap: break-word; | |
| word-wrap: break-word; | |
| word-break: break-word; | |
| white-space: normal; | |
| min-width: 0; | |
| max-width: 100%; | |
| } | |
| .message.user .message-content { | |
| background: var(--accent-gradient); | |
| color: white; | |
| border-top-right-radius: 4px; | |
| } | |
| .message.assistant .message-content { | |
| background: var(--bg-secondary); | |
| border-top-left-radius: 4px; | |
| } | |
| /* Text color overrides for user bubble */ | |
| .message.user .message-content p, | |
| .message.user .message-content li, | |
| .message.user .message-content strong, | |
| .message.user .message-content span:not(.hljs) { | |
| color: white; | |
| } | |
| .message-content p { | |
| margin-bottom: 8px; | |
| } | |
| .message-content p:last-child { | |
| margin-bottom: 0; | |
| } | |
| /* Lists inside message bubbles */ | |
| .message-content ul, | |
| .message-content ol { | |
| margin: 8px 0; | |
| padding-left: 24px; | |
| list-style-position: inside; | |
| } | |
| .message-content li { | |
| margin-bottom: 4px; | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| } | |
| .message-content li:last-child { | |
| margin-bottom: 0; | |
| } | |
| /* Numbered lists */ | |
| .message-content ol { | |
| list-style-type: decimal; | |
| } | |
| /* Bullet lists */ | |
| .message-content ul { | |
| list-style-type: disc; | |
| } | |
| /* Message Actions */ | |
| .message-wrapper { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .message-actions { | |
| display: flex; | |
| gap: 4px; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| padding: 4px 0; | |
| } | |
| .message:hover .message-actions, | |
| .message-actions:focus-within { | |
| opacity: 1; | |
| } | |
| .message.user .message-actions { | |
| justify-content: flex-end; | |
| } | |
| .action-btn { | |
| width: 28px; | |
| height: 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .action-btn:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| .action-btn svg { | |
| width: 14px; | |
| height: 14px; | |
| } | |
| /* Image Container */ | |
| .image-container { | |
| position: relative; | |
| display: inline-block; | |
| max-width: 100%; | |
| margin: 8px 0; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| .image-container img { | |
| display: block; | |
| max-width: 100%; | |
| border-radius: 12px; | |
| } | |
| /* ============================================ */ | |
| /* MESH GRADIENT IMAGE LOADING ANIMATION */ | |
| /* ============================================ */ | |
| .image-container.loading { | |
| min-height: 320px; | |
| min-width: 320px; | |
| background: #020205; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| overflow: hidden; | |
| border-radius: 12px; | |
| } | |
| /* Animated gradient blobs */ | |
| .image-container.loading .blob { | |
| position: absolute; | |
| width: 200%; | |
| height: 200%; | |
| border-radius: 50%; | |
| filter: blur(80px); | |
| mix-blend-mode: screen; | |
| opacity: 0.65; | |
| will-change: transform; | |
| } | |
| .image-container.loading .blob-1 { | |
| background: radial-gradient(circle at center, #00FFFF 0%, transparent 70%); | |
| top: -50%; | |
| left: -50%; | |
| animation: move-cyan 12s infinite alternate ease-in-out; | |
| } | |
| .image-container.loading .blob-2 { | |
| background: radial-gradient(circle at center, #2B00FF 0%, transparent 70%); | |
| bottom: -50%; | |
| right: -50%; | |
| animation: move-blue 15s infinite alternate ease-in-out; | |
| } | |
| .image-container.loading .blob-3 { | |
| background: radial-gradient(circle at center, #0066FF 0%, transparent 70%); | |
| top: 0; | |
| left: 0; | |
| animation: move-mid 10s infinite alternate ease-in-out; | |
| } | |
| .image-container.loading .blob-4 { | |
| background: radial-gradient(circle at center, #00FFD5 0%, transparent 70%); | |
| bottom: 0; | |
| left: 0; | |
| animation: move-teal 15s infinite alternate ease-in-out; | |
| } | |
| /* Blob movement animations */ | |
| @keyframes move-cyan { | |
| 0% { | |
| transform: translate3d(0, 0, 0) scale(1) rotate(0deg); | |
| } | |
| 50% { | |
| transform: translate3d(40%, 25%, 0) scale(1.4) rotate(90deg); | |
| } | |
| 100% { | |
| transform: translate3d(5%, 10%, 0) scale(1) rotate(-45deg); | |
| } | |
| } | |
| @keyframes move-blue { | |
| 0% { | |
| transform: translate3d(0, 0, 0) scale(1.3) rotate(0deg); | |
| } | |
| 50% { | |
| transform: translate3d(-50%, -35%, 0) scale(0.9) rotate(-120deg); | |
| } | |
| 100% { | |
| transform: translate3d(-15%, 20%, 0) scale(1.3) rotate(30deg); | |
| } | |
| } | |
| @keyframes move-mid { | |
| 0% { | |
| transform: translate3d(0, 0, 0) rotate(0deg) scale(1); | |
| } | |
| 50% { | |
| transform: translate3d(35%, 35%, 0) rotate(180deg) scale(1.5); | |
| } | |
| 100% { | |
| transform: translate3d(-15%, -25%, 0) rotate(360deg) scale(0.9); | |
| } | |
| } | |
| @keyframes move-teal { | |
| 0% { | |
| transform: translate3d(0, 0, 0) scale(0.9); | |
| } | |
| 50% { | |
| transform: translate3d(40%, -50%, 0) scale(1.6); | |
| } | |
| 100% { | |
| transform: translate3d(10%, 10%, 0) scale(0.9); | |
| } | |
| } | |
| /* Animated text with glitch entrance */ | |
| .loading-text-container { | |
| position: absolute; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| font-size: clamp(14px, 4vw, 18px); | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.02em; | |
| } | |
| .loading-text-container .char { | |
| display: inline-block; | |
| color: white; | |
| opacity: 0; | |
| animation: glitchReveal 0.4s steps(2, start) forwards; | |
| } | |
| .loading-text-container .space { | |
| width: 0.4em; | |
| } | |
| /* Glitch reveal entrance animation */ | |
| @keyframes glitchReveal { | |
| 0% { | |
| opacity: 0; | |
| transform: translateX(-2px); | |
| filter: brightness(2); | |
| } | |
| 20% { | |
| opacity: 0.5; | |
| transform: translateX(2px); | |
| } | |
| 40% { | |
| opacity: 0; | |
| transform: translateX(-1px); | |
| } | |
| 60% { | |
| opacity: 0.8; | |
| transform: translateX(1px); | |
| } | |
| 80% { | |
| opacity: 0.3; | |
| transform: translateX(-2px); | |
| } | |
| 100% { | |
| opacity: 1; | |
| transform: translateX(0); | |
| filter: brightness(1); | |
| text-shadow: 0 0 15px #00FFFF; | |
| } | |
| } | |
| /* Subtle breathing glow loop */ | |
| .loading-text-container .char-loop { | |
| animation: subtleGlow 4s infinite ease-in-out; | |
| } | |
| @keyframes subtleGlow { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| text-shadow: 0 0 15px rgba(0, 255, 255, 0.6); | |
| transform: scale(1); | |
| } | |
| 50% { | |
| opacity: 0.7; | |
| text-shadow: 0 0 5px rgba(0, 255, 255, 0.1); | |
| transform: scale(0.98); | |
| } | |
| } | |
| /* Vignette overlay */ | |
| .image-container.loading .vignette { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: radial-gradient(circle at center, transparent 10%, rgba(0, 0, 0, 0.5) 100%); | |
| z-index: 4; | |
| pointer-events: none; | |
| } | |
| /* Noise texture overlay */ | |
| .image-container.loading .noise { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| opacity: 0.05; | |
| pointer-events: none; | |
| z-index: 5; | |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.95' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); | |
| } | |
| .image-actions { | |
| position: absolute; | |
| bottom: 8px; | |
| right: 8px; | |
| display: flex; | |
| gap: 4px; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .image-container:hover .image-actions { | |
| opacity: 1; | |
| } | |
| .image-download-btn { | |
| padding: 6px 12px; | |
| background: rgba(0, 0, 0, 0.7); | |
| border: none; | |
| border-radius: 6px; | |
| color: white; | |
| font-size: 12px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| transition: background 0.2s; | |
| } | |
| .image-download-btn:hover { | |
| background: rgba(0, 0, 0, 0.9); | |
| } | |
| /* Code Blocks */ | |
| .code-block { | |
| margin: 16px 0; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| background: var(--code-bg); | |
| } | |
| .code-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 16px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .code-lang { | |
| font-size: 12px; | |
| color: #8b8b8b; | |
| text-transform: lowercase; | |
| } | |
| .copy-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 4px 8px; | |
| background: transparent; | |
| border: none; | |
| color: #8b8b8b; | |
| font-size: 12px; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: background 0.2s, color 0.2s; | |
| } | |
| .copy-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #fff; | |
| } | |
| .code-block pre { | |
| margin: 0; | |
| padding: 16px; | |
| overflow-x: auto; | |
| } | |
| .code-block code { | |
| font-family: 'JetBrains Mono', 'Fira Code', monospace; | |
| font-size: 13px; | |
| line-height: 1.5; | |
| } | |
| /* Inline code */ | |
| .message-content code:not(.hljs) { | |
| background: var(--bg-tertiary); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| } | |
| /* Thinking Animation */ | |
| .thinking { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 12px 0; | |
| color: var(--text-muted); | |
| font-size: 14px; | |
| } | |
| .thinking-dots { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .thinking-dots span { | |
| width: 6px; | |
| height: 6px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| animation: thinking 1.4s infinite ease-in-out; | |
| } | |
| .thinking-dots span:nth-child(1) { | |
| animation-delay: -0.32s; | |
| } | |
| .thinking-dots span:nth-child(2) { | |
| animation-delay: -0.16s; | |
| } | |
| @keyframes thinking { | |
| 0%, | |
| 80%, | |
| 100% { | |
| transform: scale(0.6); | |
| opacity: 0.5; | |
| } | |
| 40% { | |
| transform: scale(1); | |
| opacity: 1; | |
| } | |
| } | |
| /* Typing effect */ | |
| .typing-cursor::after { | |
| content: '▋'; | |
| animation: blink 1s infinite; | |
| color: var(--accent); | |
| } | |
| @keyframes blink { | |
| 0%, | |
| 50% { | |
| opacity: 1; | |
| } | |
| 51%, | |
| 100% { | |
| opacity: 0; | |
| } | |
| } | |
| /* Welcome Screen */ | |
| .welcome-screen { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| padding: 40px; | |
| text-align: center; | |
| } | |
| .welcome-logo { | |
| height: 60px; | |
| width: auto; | |
| margin-bottom: 24px; | |
| } | |
| .welcome-title { | |
| font-size: 28px; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| background: var(--accent-gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .welcome-subtitle { | |
| font-size: 16px; | |
| color: var(--text-muted); | |
| margin-bottom: 32px; | |
| } | |
| .suggestions-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| max-width: 600px; | |
| width: 100%; | |
| } | |
| .suggestion-card { | |
| padding: 16px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| text-align: left; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .suggestion-card:hover { | |
| border-color: var(--accent); | |
| box-shadow: var(--shadow); | |
| transform: translateY(-2px); | |
| } | |
| .suggestion-icon { | |
| font-size: 20px; | |
| margin-bottom: 8px; | |
| } | |
| .suggestion-text { | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| line-height: 1.4; | |
| } | |
| /* Input Area */ | |
| .input-area { | |
| padding: 16px 24px 24px; | |
| background: var(--bg-primary); | |
| } | |
| .input-container { | |
| max-width: 768px; | |
| margin: 0 auto; | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 12px; | |
| padding: 12px 16px; | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| box-shadow: var(--shadow); | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 2px rgba(45, 140, 190, 0.2); | |
| } | |
| .input-wrapper textarea { | |
| flex: 1; | |
| padding: 4px 0; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-primary); | |
| font-size: 15px; | |
| font-family: inherit; | |
| resize: none; | |
| max-height: 200px; | |
| line-height: 1.5; | |
| } | |
| .input-wrapper textarea::placeholder { | |
| color: var(--text-muted); | |
| } | |
| .input-wrapper textarea:focus { | |
| outline: none; | |
| } | |
| .send-btn { | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--accent); | |
| border: none; | |
| border-radius: 8px; | |
| color: white; | |
| cursor: pointer; | |
| transition: opacity 0.2s, transform 0.2s; | |
| flex-shrink: 0; | |
| } | |
| .send-btn:hover:not(:disabled) { | |
| transform: scale(1.05); | |
| } | |
| .send-btn:disabled { | |
| opacity: 0.4; | |
| cursor: not-allowed; | |
| } | |
| /* Tools Button */ | |
| .tools-area { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .tool-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 8px 14px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| color: var(--text-primary); | |
| font-size: 13px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .tool-btn:hover { | |
| background: var(--bg-hover); | |
| border-color: var(--accent); | |
| } | |
| .tool-btn.active { | |
| background: var(--accent); | |
| color: white; | |
| border-color: var(--accent); | |
| } | |
| .tool-btn svg { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| /* ============================================ */ | |
| /* FILE CARD - ChatGPT Style */ | |
| /* ============================================ */ | |
| .file-card { | |
| display: none; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 12px 14px; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| animation: fadeIn 0.3s ease; | |
| position: relative; | |
| max-width: 280px; | |
| } | |
| .file-card.show { | |
| display: flex; | |
| } | |
| .file-card .file-icon-wrapper { | |
| width: 40px; | |
| height: 40px; | |
| background: #2d8cbe; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .file-card .file-icon-wrapper svg { | |
| width: 20px; | |
| height: 20px; | |
| stroke: white; | |
| } | |
| .file-card .file-info { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| min-width: 0; | |
| flex: 1; | |
| } | |
| .file-card .file-name { | |
| font-size: 14px; | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .file-card .file-type { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| .file-card .remove-file { | |
| position: absolute; | |
| top: -6px; | |
| right: -6px; | |
| width: 20px; | |
| height: 20px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| color: var(--text-muted); | |
| transition: all 0.2s; | |
| } | |
| .file-card .remove-file:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| #fileInput { | |
| display: none; | |
| } | |
| /* File card in message bubble */ | |
| .message-file-card { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 12px; | |
| background: rgba(45, 140, 190, 0.15); | |
| border: 1px solid rgba(45, 140, 190, 0.3); | |
| border-radius: 10px; | |
| margin-bottom: 8px; | |
| max-width: 250px; | |
| } | |
| .message.user .message-file-card { | |
| background: rgba(255, 255, 255, 0.15); | |
| border-color: rgba(255, 255, 255, 0.2); | |
| } | |
| .message-file-card .file-icon-wrapper { | |
| width: 32px; | |
| height: 32px; | |
| background: #2d8cbe; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .message-file-card .file-icon-wrapper svg { | |
| width: 16px; | |
| height: 16px; | |
| stroke: white; | |
| } | |
| .message-file-card .file-info { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1px; | |
| min-width: 0; | |
| } | |
| .message-file-card .file-name { | |
| font-size: 13px; | |
| font-weight: 500; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .message-file-card .file-type { | |
| font-size: 11px; | |
| opacity: 0.7; | |
| } | |
| /* Input wrapper */ | |
| .input-wrapper { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 8px; | |
| padding: 12px 14px; | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| box-shadow: var(--shadow); | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| .input-wrapper:focus-within { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 2px rgba(45, 140, 190, 0.2); | |
| } | |
| .input-content { | |
| display: flex; | |
| flex-direction: column; | |
| flex: 1; | |
| gap: 10px; | |
| min-width: 0; | |
| } | |
| .input-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .attach-btn { | |
| width: 28px; | |
| height: 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: transparent; | |
| border: none; | |
| border-radius: 6px; | |
| color: var(--accent); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| flex-shrink: 0; | |
| } | |
| .attach-btn:hover { | |
| background: rgba(45, 140, 190, 0.15); | |
| color: var(--accent); | |
| transform: scale(1.1); | |
| } | |
| .attach-btn svg { | |
| width: 20px; | |
| height: 20px; | |
| stroke-width: 2.5; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| /* Copy Feedback */ | |
| .action-btn.copied { | |
| color: #10b981; | |
| border-color: #10b981; | |
| } | |
| .action-btn .check-icon { | |
| display: none; | |
| } | |
| .action-btn.copied .copy-icon { | |
| display: none; | |
| } | |
| .action-btn.copied .check-icon { | |
| display: block; | |
| } | |
| .input-footer { | |
| display: flex; | |
| justify-content: center; | |
| margin-top: 8px; | |
| } | |
| .input-footer-text { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| /* Connection Modal */ | |
| .modal-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.5); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: opacity 0.2s, visibility 0.2s; | |
| } | |
| .modal-overlay.show { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .modal { | |
| background: var(--bg-primary); | |
| border-radius: 16px; | |
| padding: 24px; | |
| width: 90%; | |
| max-width: 440px; | |
| box-shadow: var(--shadow-lg); | |
| transform: scale(0.95); | |
| transition: transform 0.2s; | |
| } | |
| .modal-overlay.show .modal { | |
| transform: scale(1); | |
| } | |
| .modal-title { | |
| font-size: 18px; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| } | |
| .modal-subtitle { | |
| font-size: 14px; | |
| color: var(--text-muted); | |
| margin-bottom: 20px; | |
| } | |
| .modal-input { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| margin-bottom: 16px; | |
| } | |
| .modal-input:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| .modal-actions { | |
| display: flex; | |
| gap: 12px; | |
| justify-content: flex-end; | |
| } | |
| .modal-btn { | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .modal-btn.secondary { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text-primary); | |
| } | |
| .modal-btn.primary { | |
| background: var(--accent); | |
| border: none; | |
| color: white; | |
| } | |
| .modal-btn:hover { | |
| opacity: 0.9; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: fixed; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| z-index: 100; | |
| transform: translateX(-100%); | |
| width: 260px; | |
| min-width: 260px; | |
| } | |
| .sidebar.open { | |
| transform: translateX(0); | |
| } | |
| /* On mobile, 'hidden' should also hide sidebar */ | |
| .sidebar.hidden { | |
| transform: translateX(-100%); | |
| } | |
| .suggestions-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-muted); | |
| } | |
| /* ============================================ */ | |
| /* NOTIFICATION TOAST - Bottom Left */ | |
| /* ============================================ */ | |
| .notification-toast { | |
| position: fixed; | |
| bottom: 24px; | |
| left: 24px; | |
| background: var(--bg-primary); | |
| border-radius: 16px; | |
| padding: 16px 20px; | |
| max-width: 380px; | |
| z-index: 9999; | |
| opacity: 0; | |
| visibility: hidden; | |
| transform: translateY(20px) scale(0.95); | |
| transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 255, 255, 0.3); | |
| } | |
| .notification-toast.show { | |
| opacity: 1; | |
| visibility: visible; | |
| transform: translateY(0) scale(1); | |
| } | |
| /* Animated glowing border */ | |
| .notification-toast::before { | |
| content: ''; | |
| position: absolute; | |
| inset: -2px; | |
| border-radius: 18px; | |
| padding: 2px; | |
| background: linear-gradient(90deg, #1e3a5f, #2d8cbe, #40c9c9, #2d8cbe, #1e3a5f); | |
| background-size: 300% 100%; | |
| animation: border-glow 3s linear infinite; | |
| -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); | |
| mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); | |
| -webkit-mask-composite: xor; | |
| mask-composite: exclude; | |
| z-index: -1; | |
| } | |
| @keyframes border-glow { | |
| 0% { | |
| background-position: 0% 50%; | |
| } | |
| 100% { | |
| background-position: 300% 50%; | |
| } | |
| } | |
| .toast-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 8px; | |
| } | |
| .toast-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 16px; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #1e3a5f 0%, #2d8cbe 50%, #40c9c9 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .toast-icon { | |
| width: 20px; | |
| height: 20px; | |
| animation: pulse-icon 2s infinite ease-in-out; | |
| } | |
| @keyframes pulse-icon { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| 50% { | |
| opacity: 0.7; | |
| transform: scale(0.9); | |
| } | |
| } | |
| .toast-close { | |
| background: none; | |
| border: none; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| padding: 4px; | |
| border-radius: 6px; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .toast-close:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| .toast-message { | |
| font-size: 14px; | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| margin-bottom: 14px; | |
| } | |
| .toast-message strong { | |
| background: linear-gradient(135deg, #2d8cbe 0%, #40c9c9 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-weight: 700; | |
| } | |
| .toast-btn { | |
| background: linear-gradient(135deg, #1e3a5f 0%, #2d8cbe 50%, #40c9c9 100%); | |
| color: white; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: 10px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| width: 100%; | |
| } | |
| .toast-btn:hover { | |
| filter: brightness(1.15); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 20px rgba(45, 140, 190, 0.4); | |
| } | |
| .toast-progress { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, #00FFFF, #00D4FF); | |
| border-radius: 0 0 16px 16px; | |
| animation: progress-shrink 10s linear forwards; | |
| } | |
| @keyframes progress-shrink { | |
| from { | |
| width: 100%; | |
| } | |
| to { | |
| width: 0%; | |
| } | |
| } | |
| /* Mobile Responsive */ | |
| @media (max-width: 480px) { | |
| .notification-toast { | |
| left: 12px; | |
| right: 12px; | |
| bottom: 12px; | |
| max-width: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <button class="new-chat-btn" onclick="newChat()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| New chat | |
| </button> | |
| </div> | |
| <div class="chat-history" id="chatHistory"> | |
| <!-- Chat history items will be added here --> | |
| </div> | |
| <div class="sidebar-footer"> | |
| <div class="connection-status"> | |
| <div class="status-dot online" id="statusDot"></div> | |
| <span id="connectionText">Running</span> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="main"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="logo-container"> | |
| <button class="icon-btn" onclick="toggleSidebar()" id="menuBtn"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="3" y1="12" x2="21" y2="12"></line> | |
| <line x1="3" y1="6" x2="21" y2="6"></line> | |
| <line x1="3" y1="18" x2="21" y2="18"></line> | |
| </svg> | |
| </button> | |
| <img src="logo.png" alt="Krish Mind" class="logo-img"> | |
| </div> | |
| <div class="header-actions"> | |
| <button class="icon-btn" onclick="toggleTheme()" title="Toggle theme"> | |
| <span id="themeIcon">🌙</span> | |
| </button> | |
| <button class="icon-btn" onclick="showConnectModal()" title="Settings"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| <path | |
| d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"> | |
| </path> | |
| </svg> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Chat Container --> | |
| <div class="chat-container" id="chatContainer"> | |
| <!-- Welcome Screen --> | |
| <div class="welcome-screen" id="welcomeScreen"> | |
| <img src="logo.png" alt="Krish Mind" class="welcome-logo"> | |
| <h1 class="welcome-title">Hello! I'm Krish Mind</h1> | |
| <p class="welcome-subtitle">Your AI assistant, developed by Krish CS</p> | |
| <div class="suggestions-grid"> | |
| <div class="suggestion-card" onclick="sendSuggestion('Explain quantum computing in simple terms')"> | |
| <div class="suggestion-icon">💡</div> | |
| <div class="suggestion-text">Explain quantum computing in simple terms</div> | |
| </div> | |
| <div class="suggestion-card" onclick="sendSuggestion('Write a Python function to sort a list')"> | |
| <div class="suggestion-icon">💻</div> | |
| <div class="suggestion-text">Write a Python function to sort a list</div> | |
| </div> | |
| <div class="suggestion-card" onclick="sendSuggestion('What makes you different from other AIs?')"> | |
| <div class="suggestion-icon">✨</div> | |
| <div class="suggestion-text">What makes you different from other AIs?</div> | |
| </div> | |
| <div class="suggestion-card" onclick="sendSuggestion('Help me learn a new programming language')"> | |
| <div class="suggestion-icon">📚</div> | |
| <div class="suggestion-text">Help me learn a new programming language</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Messages --> | |
| <div class="chat-messages" id="chatMessages" style="display: none;"></div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="input-area"> | |
| <div class="input-container"> | |
| <div class="tools-area"> | |
| <button class="tool-btn" id="imageGenBtn" onclick="toggleImageGen()"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> | |
| <circle cx="8.5" cy="8.5" r="1.5"></circle> | |
| <polyline points="21 15 16 10 5 21"></polyline> | |
| </svg> | |
| Create Image | |
| </button> | |
| <button class="tool-btn" id="fileUploadBtn" onclick="document.getElementById('fileInput').click()"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path | |
| d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"> | |
| </path> | |
| </svg> | |
| Attach | |
| </button> | |
| </div> | |
| <div class="input-wrapper"> | |
| <input type="file" id="fileInput" accept=".pdf,.txt,.doc,.docx,.ppt,.pptx,.md,.json,.csv,.xlsx,.xls" | |
| onchange="handleFileSelect(event)"> | |
| <div class="input-content"> | |
| <!-- File Card (ChatGPT Style) --> | |
| <div class="file-card" id="fileCard"> | |
| <div class="file-icon-wrapper"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> | |
| <polyline points="14 2 14 8 20 8"></polyline> | |
| </svg> | |
| </div> | |
| <div class="file-info"> | |
| <span class="file-name" id="fileName"></span> | |
| <span class="file-type">Document</span> | |
| </div> | |
| <button class="remove-file" onclick="clearStagedFile()">×</button> | |
| </div> | |
| <!-- Input Row with + button and textarea --> | |
| <div class="input-row"> | |
| <button class="attach-btn" onclick="document.getElementById('fileInput').click()" | |
| title="Attach file"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| </button> | |
| <textarea id="messageInput" placeholder="Message Krish Mind..." rows="1" | |
| onkeydown="handleKeyDown(event)" oninput="autoResize(this)"></textarea> | |
| </div> | |
| </div> | |
| <button class="send-btn" id="sendBtn" onclick="sendMessage()" disabled> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="white"> | |
| <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="input-footer"> | |
| <span class="input-footer-text">Krish Mind AI • Developed by Krish CS</span> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Settings Modal --> | |
| <div class="modal-overlay" id="connectModal"> | |
| <div class="modal"> | |
| <h3 class="modal-title">Connect with Krish CS</h3> | |
| <p class="modal-subtitle">Developer of Krish Mind AI</p> | |
| <div style="display: flex; flex-direction: column; gap: 12px; margin: 20px 0;"> | |
| <a href="https://youtube.com/@krishcs-2025?si=-BtwEntUrJ0Si1XG" target="_blank" | |
| style="display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 10px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" | |
| onmouseover="this.style.borderColor='#FF0000'" onmouseout="this.style.borderColor='var(--border)'"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="#FF0000"> | |
| <path | |
| d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z" /> | |
| </svg> | |
| <span style="font-size: 14px; font-weight: 500;">YouTube</span> | |
| </a> | |
| <a href="https://www.linkedin.com/in/krishkanth-k-79b4422a4" target="_blank" | |
| style="display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 10px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" | |
| onmouseover="this.style.borderColor='#0A66C2'" onmouseout="this.style.borderColor='var(--border)'"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="#0A66C2"> | |
| <path | |
| d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" /> | |
| </svg> | |
| <span style="font-size: 14px; font-weight: 500;">LinkedIn</span> | |
| </a> | |
| <a href="mailto:krishkanthkrce@gmail.com" | |
| style="display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 10px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" | |
| onmouseover="this.style.borderColor='#40c9c9'" onmouseout="this.style.borderColor='var(--border)'"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#40c9c9" stroke-width="2"> | |
| <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" /> | |
| <polyline points="22,6 12,13 2,6" /> | |
| </svg> | |
| <span style="font-size: 14px; font-weight: 500;">Email</span> | |
| </a> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="modal-btn primary" onclick="hideConnectModal()">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Notification Toast - Bottom Left --> | |
| <div class="notification-toast" id="notificationToast"> | |
| <div class="toast-header"> | |
| <div class="toast-title"> | |
| <svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="#2d8cbe" stroke-width="2.5"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="12" y1="8" x2="12" y2="12"></line> | |
| <line x1="12" y1="16" x2="12.01" y2="16"></line> | |
| </svg> | |
| Heads Up! | |
| </div> | |
| <button class="toast-close" onclick="closeToast()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| <p class="toast-message"> | |
| Responses may be <strong>slow</strong> due to limited CPU resources. Thank you for your patience! | |
| </p> | |
| <button class="toast-btn" onclick="closeToast()"> | |
| Let's Chat! | |
| </button> | |
| <div class="toast-progress"></div> | |
| </div> | |
| <script> | |
| // State | |
| let serverUrl = ''; // Empty = relative URL (works on HF Spaces) | |
| let isConnected = true; // Always connected when hosted on same server | |
| let isGenerating = false; // Prevent multiple requests | |
| let imageGenMode = false; // Image generation tool toggle | |
| let messages = []; // Current display messages | |
| let chatHistory = []; // Chat history (clears on refresh) | |
| let currentChatId = null; | |
| let currentAbortController = null; // For aborting ongoing requests | |
| // File Upload State | |
| let sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); | |
| let uploadedFile = null; // Currently uploaded file info | |
| // Smart Memory State (clears on refresh) | |
| let fullMessages = []; // Full conversation | |
| let conversationSummary = ''; // Compressed context | |
| const SUMMARIZE_THRESHOLD = 10; // Trigger summarization after this many messages | |
| const KEEP_RECENT = 6; // Keep this many recent messages in full context | |
| // Icons | |
| const SEND_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /></svg>'; | |
| const STOP_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>'; | |
| const COPY_ICON = '<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg><svg class="check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>'; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Configure marked to preserve line breaks | |
| marked.use({ breaks: true, gfm: true }); | |
| // Load theme | |
| const theme = localStorage.getItem('krishMindTheme') || 'dark'; | |
| document.documentElement.setAttribute('data-theme', theme); | |
| updateThemeIcon(); | |
| // Load chat history | |
| renderChatHistory(); | |
| // Auto-check connection (optional) | |
| checkConnection(); | |
| // Enable send button on input (only if not generating) | |
| document.getElementById('messageInput').addEventListener('input', (e) => { | |
| if (!isGenerating) { | |
| document.getElementById('sendBtn').disabled = !e.target.value.trim(); | |
| } | |
| }); | |
| // Show notification toast on page load | |
| showToast(); | |
| }); | |
| // Toast Notification Functions | |
| let toastTimeout = null; | |
| function showToast() { | |
| const toast = document.getElementById('notificationToast'); | |
| setTimeout(() => { | |
| toast.classList.add('show'); | |
| // Auto-dismiss after 10 seconds | |
| toastTimeout = setTimeout(() => { | |
| closeToast(); | |
| }, 10000); | |
| }, 500); | |
| } | |
| function closeToast() { | |
| const toast = document.getElementById('notificationToast'); | |
| toast.classList.remove('show'); | |
| if (toastTimeout) { | |
| clearTimeout(toastTimeout); | |
| toastTimeout = null; | |
| } | |
| } | |
| // Theme toggle | |
| function toggleTheme() { | |
| const current = document.documentElement.getAttribute('data-theme'); | |
| const next = current === 'dark' ? 'light' : 'dark'; | |
| document.documentElement.setAttribute('data-theme', next); | |
| localStorage.setItem('krishMindTheme', next); | |
| updateThemeIcon(); | |
| } | |
| function updateThemeIcon() { | |
| const theme = document.documentElement.getAttribute('data-theme'); | |
| document.getElementById('themeIcon').textContent = theme === 'dark' ? '☀️' : '🌙'; | |
| } | |
| // Settings modal | |
| function showConnectModal() { | |
| document.getElementById('connectModal').classList.add('show'); | |
| } | |
| function hideConnectModal() { | |
| document.getElementById('connectModal').classList.remove('show'); | |
| } | |
| // Sidebar toggle - works on both desktop and mobile | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const isMobile = window.innerWidth <= 768; | |
| if (isMobile) { | |
| // Mobile: toggle 'open' class (slides in from left) | |
| sidebar.classList.toggle('open'); | |
| } else { | |
| // Desktop: toggle 'hidden' class (collapses width) | |
| sidebar.classList.toggle('hidden'); | |
| } | |
| } | |
| // Close sidebar when clicking outside (mobile only) | |
| document.addEventListener('click', (e) => { | |
| const sidebar = document.getElementById('sidebar'); | |
| const menuBtn = document.getElementById('menuBtn'); | |
| const isMobile = window.innerWidth <= 768; | |
| if (isMobile && sidebar.classList.contains('open') && | |
| !sidebar.contains(e.target) && | |
| !menuBtn.contains(e.target)) { | |
| sidebar.classList.remove('open'); | |
| } | |
| }); | |
| // Check connection (silent - no error message shown) | |
| async function checkConnection() { | |
| try { | |
| const res = await fetch(serverUrl); | |
| const data = await res.json(); | |
| if (data.status === 'online') { | |
| console.log("✅ Connected to Server"); | |
| } | |
| } catch (err) { | |
| // Silent fail - just log to console | |
| console.log("ℹ️ Server status check skipped"); | |
| } | |
| } | |
| // New chat - clears everything (optimized for HF) | |
| function newChat() { | |
| // ABORT any ongoing request | |
| if (currentAbortController) { | |
| currentAbortController.abort(); | |
| currentAbortController = null; | |
| console.log('⛔ Ongoing request aborted!'); | |
| } | |
| // Reset generating state | |
| isGenerating = false; | |
| const sendBtn = document.getElementById('sendBtn'); | |
| if (sendBtn) { | |
| sendBtn.innerHTML = SEND_ICON; | |
| sendBtn.disabled = true; | |
| } | |
| // Hide thinking indicator if visible | |
| hideThinking(); | |
| // Clear display messages | |
| messages = []; | |
| // Clear smart memory | |
| fullMessages = []; | |
| conversationSummary = ''; | |
| sessionStorage.removeItem('krishMindFullMessages'); | |
| sessionStorage.removeItem('krishMindSummary'); | |
| // Clear chat history from sidebar (don't stack) | |
| chatHistory = []; | |
| sessionStorage.removeItem('krishMindChatHistory'); | |
| currentChatId = null; | |
| // Reset UI | |
| document.getElementById('welcomeScreen').style.display = 'flex'; | |
| document.getElementById('chatMessages').style.display = 'none'; | |
| document.getElementById('chatMessages').innerHTML = ''; | |
| document.getElementById('chatHistory').innerHTML = ''; | |
| console.log('🗑️ Chat cleared!'); | |
| } | |
| // Render chat history | |
| function renderChatHistory() { | |
| const container = document.getElementById('chatHistory'); | |
| container.innerHTML = chatHistory.map(chat => ` | |
| <div class="chat-history-item ${chat.id === currentChatId ? 'active' : ''}" onclick="loadChat(${chat.id})"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> | |
| </svg> | |
| ${chat.title} | |
| </div> | |
| `).join(''); | |
| } | |
| // Send message | |
| async function sendMessage() { | |
| const input = document.getElementById('messageInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const message = input.value.trim(); | |
| if (!message && !stagedFile) return; | |
| // If already generating, this is a STOP request | |
| if (isGenerating) { | |
| console.log('⛔ Stopping generation...'); | |
| if (currentAbortController) { | |
| currentAbortController.abort(); | |
| currentAbortController = null; | |
| } | |
| hideThinking(); | |
| isGenerating = false; | |
| sendBtn.innerHTML = SEND_ICON; | |
| sendBtn.disabled = !input.value.trim(); | |
| return; | |
| } | |
| if (!isConnected) { | |
| showConnectModal(); | |
| return; | |
| } | |
| // Set generating state | |
| isGenerating = true; | |
| sendBtn.innerHTML = STOP_ICON; | |
| sendBtn.disabled = false; | |
| // Hide welcome screen | |
| document.getElementById('welcomeScreen').style.display = 'none'; | |
| document.getElementById('chatMessages').style.display = 'block'; | |
| // Clear input | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| // Add user message (with file card if file attached) | |
| const attachedFileName = stagedFileName; // Capture before clearing | |
| const fileToUpload = stagedFile; // Store file reference before clearing UI | |
| addMessage('user', message, attachedFileName); | |
| // Clear file card immediately (no delay) - only clears UI, not file reference | |
| const fileCard = document.getElementById('fileCard'); | |
| fileCard.classList.remove('show'); | |
| // Scroll to bottom after adding user message | |
| const chatContainer = document.getElementById('chatContainer'); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| // Auto-detect image generation requests | |
| const imageKeywords = [ | |
| /generate\s+(an?\s+)?image/i, | |
| /create\s+(an?\s+)?image/i, | |
| /make\s+(an?\s+)?image/i, | |
| /give\s+(me\s+)?(an?\s+)?image/i, | |
| /draw\s+(me\s+)?(an?\s+)?/i, | |
| /picture\s+(of|for)/i, | |
| /image\s+(of|for)/i, | |
| /photo\s+(of|for)/i, | |
| /generate\s+me/i, | |
| /create\s+me/i, | |
| /show\s+me\s+(an?\s+)?image/i, | |
| /\bimage\b.*\bfor\b/i | |
| ]; | |
| const isImageRequest = imageKeywords.some(regex => regex.test(message)); | |
| // Check if image generation mode is active OR user asked for an image | |
| if (imageGenMode || isImageRequest) { | |
| // Extract the image prompt (remove common prefixes) | |
| let imagePrompt = message | |
| .replace(/^(generate|create|make|draw|show|give)\s+(me\s+)?(an?\s+)?(image|picture|photo)\s+(of\s+|for\s+)?/i, '') | |
| .trim() || message; | |
| // Direct image generation | |
| await generateImage(imagePrompt); | |
| // Turn off image mode if it was on | |
| if (imageGenMode) toggleImageGen(); | |
| // Save to history | |
| saveChatToHistory(message); | |
| // Reset state | |
| isGenerating = false; | |
| sendBtn.innerHTML = SEND_ICON; | |
| sendBtn.disabled = true; | |
| stagedFile = null; | |
| stagedFileName = null; | |
| return; | |
| } | |
| // Upload file if one was attached | |
| const hadFile = !!fileToUpload; | |
| if (hadFile) { | |
| console.log(`📤 Uploading file: ${attachedFileName}`); | |
| const formData = new FormData(); | |
| formData.append('file', fileToUpload); | |
| try { | |
| const response = await fetch(`${serverUrl}/upload?session_id=${sessionId}`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| console.log(`✅ File uploaded: ${data.filename} (${data.chunks} chunks)`); | |
| } else { | |
| console.error('Upload failed:', data.error); | |
| addMessage('assistant', 'Failed to process file: ' + data.error); | |
| isGenerating = false; | |
| sendBtn.innerHTML = SEND_ICON; | |
| sendBtn.disabled = true; | |
| stagedFile = null; | |
| stagedFileName = null; | |
| return; | |
| } | |
| } catch (err) { | |
| console.error('Upload error:', err); | |
| addMessage('assistant', 'Failed to upload file. Please try again.'); | |
| isGenerating = false; | |
| sendBtn.innerHTML = SEND_ICON; | |
| sendBtn.disabled = true; | |
| stagedFile = null; | |
| stagedFileName = null; | |
| return; | |
| } | |
| } | |
| // Reset stagedFile reference | |
| stagedFile = null; | |
| stagedFileName = null; | |
| // Show thinking | |
| showThinking(); | |
| try { | |
| // Smart Memory: Check if we need to summarize | |
| if (fullMessages.length >= SUMMARIZE_THRESHOLD && !conversationSummary) { | |
| console.log('📝 Triggering summarization...'); | |
| const messagesToSummarize = fullMessages.slice(0, -KEEP_RECENT); | |
| try { | |
| const sumRes = await fetch(`${serverUrl}/summarize`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ messages: messagesToSummarize }) | |
| }); | |
| const sumData = await sumRes.json(); | |
| if (sumData.summary) { | |
| conversationSummary = sumData.summary; | |
| sessionStorage.setItem('krishMindSummary', conversationSummary); | |
| // Keep only recent messages in fullMessages | |
| fullMessages = fullMessages.slice(-KEEP_RECENT); | |
| sessionStorage.setItem('krishMindFullMessages', JSON.stringify(fullMessages)); | |
| console.log('✅ Summarized! Summary:', conversationSummary.slice(0, 50) + '...'); | |
| } | |
| } catch (sumErr) { | |
| console.error('Summarization failed:', sumErr); | |
| } | |
| } | |
| // Build recent history for context (last 6 messages or less) | |
| const recentHistory = fullMessages.slice(-KEEP_RECENT); | |
| // Create abort controller for this request | |
| currentAbortController = new AbortController(); | |
| const response = await fetch(`${serverUrl}/chat`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| message: message, | |
| max_tokens: 1024, | |
| temperature: 0.7, | |
| summary: conversationSummary, | |
| history: recentHistory, | |
| session_id: sessionId | |
| }), | |
| signal: currentAbortController.signal | |
| }); | |
| const data = await response.json(); | |
| hideThinking(); | |
| if (data.response) { | |
| await typeMessage('assistant', data.response); | |
| } else if (data.error) { | |
| addMessage('assistant', `Error: ${data.error}`); | |
| } | |
| // Save to history | |
| saveChatToHistory(message); | |
| } catch (err) { | |
| hideThinking(); | |
| addMessage('assistant', 'Connection error. Is the server running?'); | |
| console.error(err); | |
| } finally { | |
| // Reset generating state | |
| isGenerating = false; | |
| sendBtn.innerHTML = SEND_ICON; | |
| sendBtn.disabled = true; | |
| // Clear server file if one was uploaded (ChatGPT style - one-time use) | |
| if (hadFile) { | |
| await clearServerFile(); | |
| } | |
| } | |
| } | |
| // Add message with action buttons | |
| function addMessage(role, content, attachedFile = null) { | |
| const container = document.getElementById('chatMessages'); | |
| const msgIndex = messages.length; | |
| // Process markdown | |
| let html = marked.parse(content); | |
| // Add code block wrappers | |
| html = html.replace(/<pre><code class="language-(\w+)">/g, (match, lang) => { | |
| return `<div class="code-block"><div class="code-header"><span class="code-lang">${lang}</span><button class="copy-btn" onclick="copyCode(this)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy</button></div><pre><code class="language-${lang}">`; | |
| }); | |
| html = html.replace(/<\/code><\/pre>/g, '</code></pre></div>'); | |
| // File card HTML for user messages (ChatGPT style) | |
| let fileCardHtml = ''; | |
| if (attachedFile && role === 'user') { | |
| fileCardHtml = ` | |
| <div class="message-file-card"> | |
| <div class="file-icon-wrapper"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> | |
| <polyline points="14 2 14 8 20 8"></polyline> | |
| </svg> | |
| </div> | |
| <div class="file-info"> | |
| <span class="file-name">${attachedFile}</span> | |
| <span class="file-type">Document</span> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // Action buttons based on role | |
| const editIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>'; | |
| let actionsHtml = ''; | |
| if (role === 'user') { | |
| actionsHtml = ` | |
| <div class="message-actions"> | |
| <button class="action-btn" onclick="copyMessage(${msgIndex}, this)" title="Copy">${COPY_ICON}</button> | |
| <button class="action-btn" onclick="editMessage(${msgIndex})" title="Edit">${editIcon}</button> | |
| </div> | |
| `; | |
| } else { | |
| actionsHtml = ` | |
| <div class="message-actions"> | |
| <button class="action-btn" onclick="copyMessage(${msgIndex}, this)" title="Copy">${COPY_ICON}</button> | |
| </div> | |
| `; | |
| } | |
| const div = document.createElement('div'); | |
| div.className = `message ${role}`; | |
| div.setAttribute('data-index', msgIndex); | |
| div.innerHTML = ` | |
| <div class="message-wrapper"> | |
| ${fileCardHtml} | |
| <div class="message-content">${html}</div> | |
| ${actionsHtml} | |
| </div> | |
| `; | |
| container.appendChild(div); | |
| // Scroll to bottom | |
| const chatContainer = document.getElementById('chatContainer'); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| // Highlight code | |
| div.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block)); | |
| messages.push({ role, content, attachedFile }); | |
| // Smart Memory: Save to fullMessages (in-memory only, clears on refresh) | |
| fullMessages.push({ role, content }); | |
| } | |
| // Copy message content with tick feedback | |
| function copyMessage(index, btn) { | |
| const msg = messages[index]; | |
| if (msg) { | |
| navigator.clipboard.writeText(msg.content); | |
| // Show tick feedback | |
| if (btn) { | |
| btn.classList.add('copied'); | |
| setTimeout(() => btn.classList.remove('copied'), 2000); | |
| } | |
| } | |
| } | |
| // Edit user message | |
| function editMessage(index) { | |
| const msg = messages[index]; | |
| if (msg && msg.role === 'user') { | |
| document.getElementById('messageInput').value = msg.content; | |
| document.getElementById('messageInput').focus(); | |
| document.getElementById('sendBtn').disabled = false; | |
| } | |
| } | |
| // Toggle image generation mode | |
| function toggleImageGen() { | |
| imageGenMode = !imageGenMode; | |
| const btn = document.getElementById('imageGenBtn'); | |
| const input = document.getElementById('messageInput'); | |
| if (imageGenMode) { | |
| btn.classList.add('active'); | |
| input.placeholder = 'Describe the image you want to create...'; | |
| } else { | |
| btn.classList.remove('active'); | |
| input.placeholder = 'Message Krish Mind...'; | |
| } | |
| } | |
| // Generate image directly (frontend-triggered) | |
| async function generateImage(prompt) { | |
| const chatContainer = document.getElementById('chatContainer'); | |
| const messagesContainer = document.getElementById('chatMessages'); | |
| // Create loading message with mesh gradient animation | |
| const div = document.createElement('div'); | |
| div.className = 'message assistant'; | |
| div.innerHTML = ` | |
| <div class="message-wrapper"> | |
| <div class="message-content"> | |
| <p>Generating image: "${prompt}"</p> | |
| <div class="image-container loading"> | |
| <div class="blob blob-1"></div> | |
| <div class="blob blob-2"></div> | |
| <div class="blob blob-3"></div> | |
| <div class="blob blob-4"></div> | |
| <div class="vignette"></div> | |
| <div class="noise"></div> | |
| <div class="loading-text-container" id="loadingText-${Date.now()}"></div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| messagesContainer.appendChild(div); | |
| // Animate the "CREATING IMAGE" text with glitch effect | |
| const textContainer = div.querySelector('.loading-text-container'); | |
| const text = "CREATING IMAGE"; | |
| [...text].forEach((char, i) => { | |
| const span = document.createElement('span'); | |
| if (char === ' ') { | |
| span.className = 'space'; | |
| } else { | |
| span.className = 'char'; | |
| span.innerText = char; | |
| span.style.setProperty('--i', i); | |
| span.style.animationDelay = `${i * 0.05}s`; | |
| // Switch to breathing loop after entrance | |
| span.addEventListener('animationend', (e) => { | |
| if (e.animationName === 'glitchReveal') { | |
| span.classList.add('char-loop'); | |
| } | |
| }); | |
| } | |
| textContainer.appendChild(span); | |
| }); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| // Create Pollinations URL | |
| const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=512&height=512&nologo=true`; | |
| // Load image | |
| const container = div.querySelector('.image-container'); | |
| const img = new Image(); | |
| img.onload = () => { | |
| container.classList.remove('loading'); | |
| container.innerHTML = ` | |
| <img src="${imageUrl}" alt="${prompt}"> | |
| <div class="image-actions"> | |
| <button class="image-download-btn" onclick="downloadImage('${imageUrl}', '${prompt.replace(/'/g, "\\'")}')"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7 10 12 15 17 10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| Download | |
| </button> | |
| </div> | |
| `; | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| }; | |
| img.onerror = () => { | |
| container.classList.remove('loading'); | |
| container.innerHTML = `<p style="color:var(--text-muted);padding:12px;">Failed to generate image. Try again.</p>`; | |
| }; | |
| img.src = imageUrl; | |
| // Store in messages | |
| messages.push({ role: 'assistant', content: `` }); | |
| } | |
| // Type message with animation (renders markdown progressively) | |
| async function typeMessage(role, content) { | |
| const chatContainer = document.getElementById('chatContainer'); | |
| const messagesContainer = document.getElementById('chatMessages'); | |
| const msgIndex = messages.length; | |
| const div = document.createElement('div'); | |
| div.className = 'message assistant'; | |
| div.setAttribute('data-index', msgIndex); | |
| div.innerHTML = `<div class="message-wrapper"><div class="message-content"></div></div>`; | |
| messagesContainer.appendChild(div); | |
| const messageWrapper = div.querySelector('.message-wrapper'); | |
| const messageContent = div.querySelector('.message-content'); | |
| let displayedText = ''; | |
| let i = 0; | |
| const speed = 8; | |
| return new Promise(resolve => { | |
| function type() { | |
| if (i < content.length) { | |
| displayedText += content.charAt(i); | |
| i++; | |
| // Render markdown progressively | |
| let html = marked.parse(displayedText + '\u258b'); | |
| // Handle code blocks | |
| html = html.replace(/<pre><code class="language-(\w+)">/g, (match, lang) => { | |
| return `<div class="code-block"><div class="code-header"><span class="code-lang">${lang}</span><button class="copy-btn" onclick="copyCode(this)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy</button></div><pre><code class="language-${lang}">`; | |
| }); | |
| html = html.replace(/<\/code><\/pre>/g, '</code></pre></div>'); | |
| // Handle images - show loading container first | |
| html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { | |
| return `<div class="image-container loading" data-src="${src}" data-alt="${alt}"></div>`; | |
| }); | |
| messageContent.innerHTML = html; | |
| // Highlight any code that's complete | |
| div.querySelectorAll('pre code').forEach(block => { | |
| if (!block.classList.contains('hljs')) { | |
| hljs.highlightElement(block); | |
| } | |
| }); | |
| // Auto-scroll chat container | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| setTimeout(type, speed); | |
| } else { | |
| // Finished - render final version | |
| let html = marked.parse(content); | |
| html = html.replace(/<pre><code class="language-(\w+)">/g, (match, lang) => { | |
| return `<div class="code-block"><div class="code-header"><span class="code-lang">${lang}</span><button class="copy-btn" onclick="copyCode(this)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy</button></div><pre><code class="language-${lang}">`; | |
| }); | |
| html = html.replace(/<\/code><\/pre>/g, '</code></pre></div>'); | |
| // Handle images - show loading container | |
| html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { | |
| return `<div class="image-container loading" data-src="${src}" data-alt="${alt}"></div>`; | |
| }); | |
| messageContent.innerHTML = html; | |
| // Add copy button for AI response | |
| const copyIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>'; | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'message-actions'; | |
| actionsDiv.innerHTML = `<button class="action-btn" onclick="copyMessage(${msgIndex})" title="Copy">${copyIcon}</button>`; | |
| messageWrapper.appendChild(actionsDiv); | |
| // Load images properly | |
| div.querySelectorAll('.image-container.loading').forEach(container => { | |
| const src = container.dataset.src; | |
| const alt = container.dataset.alt; | |
| const img = new Image(); | |
| img.onload = () => { | |
| container.classList.remove('loading'); | |
| container.innerHTML = ` | |
| <img src="${src}" alt="${alt}"> | |
| <div class="image-actions"> | |
| <button class="image-download-btn" onclick="downloadImage('${src}', '${alt || 'image'}')"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7 10 12 15 17 10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| Download | |
| </button> | |
| </div> | |
| `; | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| }; | |
| img.onerror = () => { | |
| container.classList.remove('loading'); | |
| container.innerHTML = `<p style="color:var(--text-muted);padding:12px;">Failed to load image</p>`; | |
| }; | |
| img.src = src; | |
| }); | |
| div.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block)); | |
| messages.push({ role, content }); | |
| // Smart Memory: Save to fullMessages and persist | |
| fullMessages.push({ role, content }); | |
| sessionStorage.setItem('krishMindFullMessages', JSON.stringify(fullMessages)); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| resolve(); | |
| } | |
| } | |
| type(); | |
| }); | |
| } | |
| // Download image | |
| function downloadImage(url, filename) { | |
| fetch(url) | |
| .then(response => response.blob()) | |
| .then(blob => { | |
| const link = document.createElement('a'); | |
| link.href = URL.createObjectURL(blob); | |
| link.download = filename.replace(/[^a-zA-Z0-9]/g, '_') + '.png'; | |
| link.click(); | |
| URL.revokeObjectURL(link.href); | |
| }) | |
| .catch(err => { | |
| console.error('Download failed:', err); | |
| // Fallback: open in new tab | |
| window.open(url, '_blank'); | |
| }); | |
| } | |
| // Thinking animation | |
| function showThinking() { | |
| const container = document.getElementById('chatMessages'); | |
| const div = document.createElement('div'); | |
| div.className = 'message assistant'; | |
| div.id = 'thinking'; | |
| div.innerHTML = ` | |
| <div class="message-content"> | |
| <div class="thinking"> | |
| <div class="thinking-dots"><span></span><span></span><span></span></div> | |
| <span>Thinking...</span> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(div); | |
| // Scroll main container to bottom | |
| const chatContainer = document.getElementById('chatContainer'); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function hideThinking() { | |
| const thinking = document.getElementById('thinking'); | |
| if (thinking) thinking.remove(); | |
| } | |
| // Copy code | |
| function copyCode(btn) { | |
| const code = btn.closest('.code-block').querySelector('code').textContent; | |
| navigator.clipboard.writeText(code); | |
| btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg> Copied!'; | |
| setTimeout(() => { | |
| btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy'; | |
| }, 2000); | |
| } | |
| // Send suggestion | |
| function sendSuggestion(text) { | |
| document.getElementById('messageInput').value = text; | |
| document.getElementById('sendBtn').disabled = false; | |
| sendMessage(); | |
| } | |
| // Handle keyboard | |
| function handleKeyDown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| // Auto resize textarea | |
| function autoResize(el) { | |
| el.style.height = 'auto'; | |
| el.style.height = Math.min(el.scrollHeight, 200) + 'px'; | |
| } | |
| // Save chat to history | |
| function saveChatToHistory(firstMessage) { | |
| if (!currentChatId) { | |
| currentChatId = Date.now(); | |
| chatHistory.unshift({ | |
| id: currentChatId, | |
| title: firstMessage.slice(0, 30) + (firstMessage.length > 30 ? '...' : ''), | |
| timestamp: new Date().toISOString() | |
| }); | |
| sessionStorage.setItem('krishMindChatHistory', JSON.stringify(chatHistory.slice(0, 20))); | |
| renderChatHistory(); | |
| } | |
| } | |
| // Close modal on outside click | |
| document.getElementById('connectModal').addEventListener('click', (e) => { | |
| if (e.target.id === 'connectModal') hideConnectModal(); | |
| }); | |
| // ========================================== | |
| // FILE UPLOAD FUNCTIONS (ChatGPT Style) | |
| // ========================================== | |
| let stagedFile = null; // File staged for upload with next message | |
| let stagedFileName = null; // Store filename for message display | |
| function handleFileSelect(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| // Stage the file (don't upload yet) | |
| stagedFile = file; | |
| stagedFileName = file.name; | |
| // Show file card in input area | |
| const fileCard = document.getElementById('fileCard'); | |
| const fileNameEl = document.getElementById('fileName'); | |
| fileNameEl.textContent = file.name; | |
| fileCard.classList.add('show'); | |
| // Enable send button since we have a file | |
| document.getElementById('sendBtn').disabled = false; | |
| console.log(`📎 File staged: ${file.name}`); | |
| // Clear input so same file can be re-selected | |
| event.target.value = ''; | |
| } | |
| function clearStagedFile() { | |
| stagedFile = null; | |
| stagedFileName = null; | |
| const fileCard = document.getElementById('fileCard'); | |
| fileCard.classList.remove('show'); | |
| // Disable send button if no text | |
| const input = document.getElementById('messageInput'); | |
| document.getElementById('sendBtn').disabled = !input.value.trim(); | |
| console.log('📎 Staged file cleared'); | |
| } | |
| async function uploadStagedFile() { | |
| if (!stagedFile) return true; // No file to upload | |
| try { | |
| const formData = new FormData(); | |
| formData.append('file', stagedFile); | |
| console.log(`📤 Uploading ${stagedFile.name}...`); | |
| const response = await fetch(`${serverUrl}/upload?session_id=${sessionId}`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| console.log(`✅ File uploaded: ${data.filename} (${data.chunks} chunks)`); | |
| return true; | |
| } else { | |
| console.error('Upload failed:', data.error); | |
| return false; | |
| } | |
| } catch (err) { | |
| console.error('Upload error:', err); | |
| return false; | |
| } | |
| } | |
| async function clearServerFile() { | |
| try { | |
| await fetch(`${serverUrl}/upload/${sessionId}`, { | |
| method: 'DELETE' | |
| }); | |
| } catch (err) { | |
| // Ignore errors when clearing | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |