Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| st.set_page_config(layout="wide", page_title="Streamlit LLM Playground") | |
| st.markdown(""" | |
| <style> | |
| #MainMenu, header, footer { visibility: hidden; } | |
| html, body, .main { | |
| height: 100%; | |
| width: 100%; | |
| margin: 5px; | |
| color :white; | |
| overflow: hidden !important; | |
| } | |
| iframe { | |
| height:690px !important; | |
| width: 99.5vw !important; | |
| border-radius: 5px ; | |
| overflow: hidden !important; | |
| margin-top:-15px; | |
| } | |
| div[data-testid="stMainBlockContainer"]{ | |
| padding :0px !important; | |
| margin-top:-15px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| html_code=""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LLM Studio Enhanced</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |
| <style> | |
| /* General Styles */ | |
| :root { | |
| --bg-color-dark: #0D1117; | |
| --bg-color-medium: #161B22; | |
| --border-color: #30363D; | |
| --text-color-primary: #c9d1d9; | |
| --text-color-secondary: #8b949e; | |
| --accent-color: #58a6ff; | |
| --accent-glow: rgba(88, 166, 255, 0.3); | |
| --user-bubble-bg: #21262d; | |
| --model-bubble-bg: #161B22; | |
| --error-color: #f85149; | |
| --groq-color: #4CAF50; | |
| --container-radius: 16px; | |
| } | |
| body { | |
| height: 100vh; | |
| width: 100%; | |
| margin: 0; | |
| padding: 0; | |
| background: var(--bg-color-dark); | |
| font-family: 'time new roman'; | |
| color: var(--text-color-primary); | |
| } | |
| .studio-container { | |
| width: 100%; | |
| height: 100vh; | |
| background-color: rgba(22, 27, 34, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-radius: var(--container-radius); | |
| border: 1px solid var(--border-color); | |
| overflow: hidden; | |
| display: flex; | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); | |
| } | |
| /* MODIFIED: Final Polished Collapsible Sidebar */ | |
| .sidebar { | |
| width: 240px; /* Fixed width */ | |
| background-color: rgba(13, 17, 23, 0.9); | |
| border-right: 1px solid var(--border-color); | |
| flex-shrink: 0; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| padding: 0; | |
| box-shadow: 4px 0 20px -5px rgba(0,0,0,0.3), 1px 0 0 0 rgba(88, 166, 255, 0.1); | |
| border-top-left-radius: var(--container-radius); | |
| border-bottom-left-radius: var(--container-radius); | |
| } | |
| .sidebar-content { | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| padding: 16px; | |
| min-width: 208px; | |
| } | |
| .sidebar-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding-bottom: 24px; | |
| border-bottom: 1px solid var(--border-color); | |
| margin-bottom: 24px; | |
| flex-shrink: 0; | |
| } | |
| .sidebar-header .logo { | |
| font-size: 28px; | |
| color: var(--accent-color); | |
| text-shadow: 0 0 10px var(--accent-glow); | |
| min-width: 36px; | |
| text-align: center; | |
| } | |
| .new-chat-btn { | |
| background: linear-gradient(135deg, #58a6ff, #3a8dff); | |
| border: none; | |
| color: #ffffff; | |
| padding: 12px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: flex-start; | |
| gap: 16px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| margin-bottom: 24px; | |
| box-shadow: 0 4px 15px rgba(88, 166, 255, 0.2); | |
| } | |
| .new-chat-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(88, 166, 255, 0.3); | |
| } | |
| .nav-menu .nav-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| padding: 12px; | |
| border-radius: 8px; | |
| margin-bottom: 8px; | |
| cursor: pointer; | |
| transition: background-color 0.3s ease, color 0.3s ease; | |
| font-weight: 500; | |
| color: var(--text-color-primary); | |
| } | |
| .nav-menu .nav-item:hover { background-color: var(--bg-color-medium); } | |
| .nav-menu .nav-item.active { | |
| background-color: var(--accent-color); | |
| color: var(--bg-color-dark); | |
| font-weight: 600; | |
| box-shadow: 0 0 15px var(--accent-glow); | |
| } | |
| .nav-menu .nav-item.active .material-icons { color: var(--bg-color-dark); } | |
| .nav-menu .nav-item .material-icons { | |
| color: var(--text-color-secondary); | |
| transition: color 0.3s ease; | |
| min-width: 24px; | |
| } | |
| .nav-menu .nav-item:hover .material-icons { color: var(--text-color-primary); } | |
| .item-text { | |
| opacity: 1; /* Always visible */ | |
| width: auto; /* Always auto width */ | |
| white-space: nowrap; | |
| } | |
| .history-section { | |
| margin-top: 24px; | |
| padding-top: 24px; | |
| border-top: 1px solid var(--border-color); | |
| flex-grow: 1; | |
| overflow-y: auto; | |
| min-height: 100px; | |
| } | |
| .history-title { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-color-secondary); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 12px; | |
| padding-left: 12px; | |
| } | |
| .chat-history-list .history-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 10px 12px; | |
| font-size: 14px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| margin-bottom: 4px; | |
| color: var(--text-color-secondary); | |
| position: relative; | |
| } | |
| .chat-history-list .history-item:hover { | |
| background-color: var(--bg-color-medium); | |
| color: var(--text-color-primary); | |
| } | |
| .chat-history-list .history-item.active { | |
| background-color: var(--user-bubble-bg); | |
| color: var(--text-color-primary); | |
| font-weight: 500; | |
| } | |
| .history-item .history-title-text { | |
| flex-grow: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| margin-left: 16px; | |
| } | |
| .response-content{ | |
| margin-top:-40px; | |
| } | |
| .history-item .history-title-input { | |
| width: 100%; | |
| background: var(--bg-color-dark); | |
| border: 1px solid var(--accent-color); | |
| border-radius: 4px; | |
| color: var(--text-color-primary); | |
| padding: 2px 4px; | |
| } | |
| .history-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-left: 8px; | |
| } | |
| .history-item:hover .history-actions { | |
| display: flex; | |
| } | |
| .history-icon { | |
| font-size: 16px; | |
| cursor: pointer; | |
| color: var(--text-color-secondary); | |
| } | |
| .history-icon:hover { | |
| color: var(--accent-color); | |
| } | |
| .api-key-section { | |
| padding-top: 24px; | |
| margin-top: 24px; | |
| border-top: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| } | |
| .api-key-group { | |
| margin-bottom: 12px; | |
| } | |
| .api-key-group:last-child { | |
| margin-bottom: 0; | |
| } | |
| .api-key-section label { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-color-primary); | |
| letter-spacing: 0.5px; | |
| margin-bottom: 6px; | |
| display: block; | |
| } | |
| .api-key-section input { | |
| width: 100%; | |
| box-sizing: border-box; | |
| background-color: var(--bg-color-dark); | |
| border: 1px solid var(--border-color); | |
| border-radius: 6px; | |
| padding: 8px; | |
| color: var(--text-color-primary); | |
| font-size: 12px; | |
| transition: border-color 0.3s, box-shadow 0.3s; | |
| } | |
| .api-key-section input:focus { | |
| outline: none; | |
| border-color: var(--accent-color); | |
| box-shadow: 0 0 8px var(--accent-glow); | |
| } | |
| .input-error { | |
| border-color: var(--error-color) !important; | |
| box-shadow: 0 0 10px rgba(248, 81, 73, 0.5) !important; | |
| } | |
| .main-content { | |
| flex-grow: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| background-image: radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.05), transparent 40%); | |
| } | |
| .content-window { | |
| flex-grow: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .prompt-view { | |
| display: none; | |
| } | |
| .prompt-view.active { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #home-screen { | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| padding: 40px; | |
| display: none; | |
| } | |
| #home-screen.active { display: flex; } | |
| #home-screen .logo { font-size: 60px; color: var(--accent-color); } | |
| #home-screen h1 { font-size: 32px; margin: 16px 0 8px; color: var(--text-color-primary); } | |
| #home-screen p { color: var(--text-color-secondary); max-width: 450px; line-height: 1.6; } | |
| /* MODIFIED: chat-history-display is now a simple container */ | |
| .chat-history-display { | |
| flex-grow: 1; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* ADDED: New class for individual chat session containers */ | |
| .chat-session-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| padding: 24px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| box-sizing: border-box; | |
| } | |
| .chat-message-container { | |
| display: flex; | |
| flex-direction: column; | |
| max-width: 85%; | |
| } | |
| .chat-message { | |
| display: flex; | |
| gap: 6px; | |
| align-items: flex-start; | |
| } | |
| .chat-message.user { justify-content: flex-end; } | |
| .chat-message-container.user { align-items: flex-end; } | |
| .chat-message-container.model { align-items: flex-start; } | |
| .chat-avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| font-weight: 600; | |
| border: 1px solid var(--border-color); | |
| } | |
| .chat-message.user .chat-avatar { background-color: var(--bg-color-medium); } | |
| .chat-message.model .chat-avatar { background-color: var(--accent-color); } | |
| .chat-message.user .chat-avatar .material-icons { color: var(--text-color-primary); } | |
| .chat-message.model .chat-avatar .material-icons { color: var(--bg-color-dark); font-size: 20px; } | |
| .chat-bubble { | |
| padding: 12px 18px; | |
| border-radius: 20px; | |
| max-width: 100%; | |
| line-height: 1.6; | |
| word-wrap: break-word; | |
| white-space: pre-wrap; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| border: 1px solid transparent; | |
| position: relative; | |
| } | |
| .copy-icon-bubble { | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| color: var(--text-color-secondary); | |
| background-color: var(--bg-color-dark); | |
| border-radius: 50%; | |
| padding: 4px; | |
| cursor: pointer; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| font-size: 18px; | |
| } | |
| .chat-bubble:hover .copy-icon-bubble { | |
| opacity: 1; | |
| } | |
| .copy-icon-bubble:hover { | |
| color: var(--accent-color); | |
| } | |
| .model-response-header { | |
| font-size: 10px; | |
| font-weight: 500; | |
| color: var(--text-color-secondary); | |
| margin-bottom: 4px; | |
| padding-bottom: 4px; | |
| margin-top:-50px; | |
| display: flex; | |
| gap: 2px; | |
| align-items: center; | |
| opacity: 0.7; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .model-response-header .material-icons { font-size: 12px; } | |
| .chat-message.user .chat-bubble { | |
| background: linear-gradient(135deg, #58a6ff, #3a8dff); | |
| color: #ffffff; | |
| border-radius: 20px 4px 20px 20px; | |
| } | |
| .chat-message.model .chat-bubble { | |
| background-color: var(--user-bubble-bg); | |
| color: var(--text-color-primary); | |
| border-color: var(--border-color); | |
| border-radius: 4px 20px 20px 20px; | |
| } | |
| .chat-message.error .chat-bubble { | |
| background-color: rgba(248, 81, 73, 0.1); | |
| border-color: var(--error-color); | |
| color: var(--error-color); | |
| } | |
| .chat-bubble pre { | |
| background: var(--bg-color-dark) !important; | |
| padding: 12px; | |
| border-radius: 8px; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 14px; | |
| margin: 8px 0 0 0; | |
| } | |
| .chat-bubble img { | |
| max-width: 100%; | |
| border-radius: 8px; | |
| margin-top: 8px; | |
| } | |
| .prompt-actions-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 8px 0 0 0; | |
| justify-content: flex-end; | |
| margin-right: 52px; | |
| } | |
| .prompt-actions-bar .action-icon { | |
| cursor: pointer; | |
| color: var(--text-color-secondary); | |
| transition: color 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 13px; | |
| } | |
| .prompt-actions-bar .action-icon:hover { | |
| color: var(--accent-color); | |
| } | |
| .prompt-actions-bar .action-icon .material-icons { | |
| font-size: 18px; | |
| } | |
| .prompt-area { | |
| padding: 16px 24px; | |
| border-top: 1px solid var(--border-color); | |
| background: var(--bg-color-dark); | |
| margin-top: auto; | |
| } | |
| .prompt-examples { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .prompt-example-btn { | |
| background: var(--bg-color-medium); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-color-secondary); | |
| padding: 6px 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 12px; | |
| } | |
| .prompt-example-btn:hover { | |
| border-color: var(--accent-color); | |
| color: var(--text-color-primary); | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| gap: 12px; | |
| align-items: flex-end; | |
| } | |
| textarea { | |
| flex-grow: 1; | |
| background-color: var(--bg-color-dark); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| padding: 12px 16px; | |
| color: var(--text-color-primary); | |
| font-family: 'Inter', sans-serif; | |
| font-size: 16px; | |
| resize: none; | |
| height: 50px; | |
| transition: height 0.2s; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: var(--accent-color); | |
| box-shadow: 0 0 8px var(--accent-glow); | |
| } | |
| .image-uploader { | |
| width: 50px; | |
| height: 50px; | |
| border: 2px dashed var(--border-color); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: border-color 0.3s ease; | |
| flex-shrink: 0; | |
| overflow: hidden; | |
| } | |
| .image-uploader:hover { border-color: var(--accent-color); } | |
| .image-uploader .material-icons { font-size: 24px; color: var(--text-color-secondary); transition: opacity 0.2s; } | |
| .image-uploader.loading .spinner { display: block; } | |
| .image-uploader.loading .material-icons, .image-uploader.has-image .material-icons { display: none; } | |
| .spinner { | |
| display: none; | |
| width: 24px; | |
| height: 24px; | |
| border: 3px solid var(--border-color); | |
| border-top-color: var(--accent-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| .image-preview { | |
| display: none; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .image-uploader.has-image .image-preview { display: block; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .right-panel { | |
| width: 250px; | |
| padding: 24px; | |
| border-left: 1px solid var(--border-color); | |
| background-color: rgba(13, 17, 23, 0.9); | |
| flex-shrink: 0; | |
| display: flex; | |
| flex-direction: column; | |
| box-shadow: -4px 0 20px -5px rgba(0,0,0,0.3), -1px 0 0 0 rgba(88, 166, 255, 0.1); | |
| border-top-right-radius: var(--container-radius); | |
| border-bottom-right-radius: var(--container-radius); | |
| } | |
| .panel-header { | |
| font-size: 14px; | |
| color: var(--text-color-secondary); | |
| font-weight: 500; | |
| margin-bottom: 16px; | |
| } | |
| .right-panel ul { list-style: none; padding: 0; margin: 0; } | |
| .model-item { | |
| padding: 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| transition: background-color 0.3s; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| color: var(--text-color-primary); | |
| position: relative; | |
| padding-left: 30px; | |
| } | |
| .model-item:hover { background-color: var(--bg-color-medium); } | |
| .model-item.active { | |
| background-color: var(--accent-color); | |
| color: #ffffff; | |
| font-weight: 600; | |
| } | |
| .model-item.groq-model::before { | |
| content: 'G'; | |
| position: absolute; | |
| left: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| font-weight: bold; | |
| color: var(--groq-color); | |
| font-size: 14px; | |
| } | |
| .parameters-section { | |
| margin-top: 24px; | |
| padding-top: 24px; | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .parameter-control { margin-bottom: 16px; } | |
| .parameter-control label { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 14px; | |
| margin-bottom: 8px; | |
| color: var(--text-color-primary); | |
| } | |
| .parameter-control label span:last-child { | |
| font-weight: 600; | |
| color: var(--text-color-primary); | |
| } | |
| .parameter-control input[type="range"] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 4px; | |
| background: var(--border-color); | |
| border-radius: 2px; | |
| outline: none; | |
| } | |
| .parameter-control input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: var(--accent-color); | |
| cursor: pointer; | |
| border-radius: 50%; | |
| box-shadow: 0 0 8px var(--accent-glow); | |
| } | |
| .action-button { | |
| background-color: var(--accent-color); | |
| border: none; | |
| color: white; | |
| padding: 0 16px; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: background-color 0.3s, box-shadow 0.3s; | |
| height: 50px; | |
| flex-shrink: 0; | |
| } | |
| .action-button:hover { | |
| background-color: #4a9eff; | |
| box-shadow: 0 0 15px var(--accent-glow); | |
| } | |
| .action-button:disabled { | |
| background-color: var(--border-color); | |
| cursor: not-allowed; | |
| box-shadow: none; | |
| } | |
| .action-button.stop-generating { | |
| background-color: var(--error-color); | |
| } | |
| .action-button.stop-generating:hover { | |
| background-color: #d9443b; | |
| box-shadow: 0 0 15px rgba(248, 81, 73, 0.5); | |
| } | |
| @media (max-width: 1100px) { | |
| .studio-container { flex-direction: column; height: auto; max-height: none; } | |
| .sidebar { | |
| width: 100%; | |
| border-right: none; border-bottom: 1px solid var(--border-color); | |
| flex-direction: row; align-items: center; justify-content: space-between; padding: 16px; | |
| overflow: visible; | |
| } | |
| .sidebar:hover { width: 100%; } | |
| .sidebar-header { border: none; margin: 0; padding: 0; } | |
| .nav-menu { display: flex; gap: 8px; } | |
| .nav-menu .nav-item { flex-direction: column; padding: 8px; gap: 4px; font-size: 12px; } | |
| .right-panel { width: 100%; border-left: none; border-top: 1px solid var(--border-color); } | |
| } | |
| @media (max-width: 768px) { | |
| body { padding: 0; } | |
| .studio-container { border-radius: 0; min-height: 100vh; } | |
| .sidebar { flex-direction: column; align-items: stretch; } | |
| .nav-menu { justify-content: center; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="studio-container"> | |
| <aside class="sidebar"> | |
| <div class="sidebar-content"> | |
| <div class="sidebar-header"> | |
| <span class="material-icons logo">auto_awesome</span><h1 class="item-text">LLM Studio</h1> | |
| </div> | |
| <button class="new-chat-btn" id="new-chat-btn"><span class="material-icons">add_circle</span><span class="item-text">New Chat</span></button> | |
| <nav class="nav-menu"> | |
| <div class="nav-item active" data-target="text-generation"><span class="material-icons">text_fields</span><span class="item-text">Text Generation</span></div> | |
| <div class="nav-item" data-target="image-to-text"><span class="material-icons">image</span><span class="item-text">Image to Text</span></div> | |
| <div class="nav-item" data-target="text-classification"><span class="material-icons">label</span><span class="item-text">Text Classification</span></div> | |
| </nav> | |
| <div class="history-section"> | |
| <h3 class="history-title item-text">History</h3> | |
| <div class="chat-history-list" id="chat-history-list"> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <main class="main-content"> | |
| <div id="home-screen" class="content-window active"> | |
| <span class="material-icons logo">auto_awesome</span> | |
| <h1>Welcome to LLM Studio</h1> | |
| <p>Select a mode from the left, or click "New Chat" to begin a new conversation. Your chat history will be saved here for this session.</p> | |
| </div> | |
| <div id="chat-view" class="content-window" style="display:none;"> | |
| <div class="chat-history-display" id="chat-history-display"></div> | |
| <div id="prompt-container"> | |
| <div id="text-generation" class="prompt-view active"> | |
| <div class="prompt-area"> | |
| <div class="prompt-examples" id="text-generation-examples"></div> | |
| <div class="input-wrapper"> | |
| <textarea id="text-generation-prompt" placeholder="Enter your prompt here..."></textarea> | |
| <button class="action-button" data-target="text-generation"><span class="material-icons">send</span></button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="image-to-text" class="prompt-view"> | |
| <div class="prompt-area"> | |
| <div class="prompt-examples" id="image-to-text-examples"></div> | |
| <div class="input-wrapper"> | |
| <div id="image-uploader" class="image-uploader"> | |
| <input type="file" id="image-upload-input" accept="image/*" style="display: none;"> | |
| <span class="material-icons">add_photo_alternate</span> | |
| <div class="spinner"></div> | |
| <img class="image-preview" id="image-preview" src="" alt="Image Preview"/> | |
| </div> | |
| <textarea id="image-to-text-prompt" placeholder="Optionally, add instructions..."></textarea> | |
| <button class="action-button" data-target="image-to-text" disabled><span class="material-icons">send</span></button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="text-classification" class="prompt-view"> | |
| <div class="prompt-area"> | |
| <div class="prompt-examples" id="text-classification-examples"></div> | |
| <div class="input-wrapper"> | |
| <textarea id="text-classification-prompt" placeholder="Enter text to classify..."></textarea> | |
| <button class="action-button" data-target="text-classification"><span class="material-icons">send</span></button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <aside class="right-panel"> | |
| <div> | |
| <p class="panel-header">MODELS</p> | |
| <ul id="model-list"> | |
| </ul> | |
| </div> | |
| <div class="parameters-section"> | |
| <p class="panel-header">PARAMETERS</p> | |
| <div class="parameter-control"> | |
| <label for="temperature"><span>Temperature</span><span id="temperature-value">0.9</span></label> | |
| <input type="range" id="temperature" min="0" max="1" step="0.1" value="0.9"> | |
| </div> | |
| <div class="parameter-control"> | |
| <label for="top-p"><span>Top-P</span><span id="top-p-value">1.0</span></label> | |
| <input type="range" id="top-p" min="0" max="1" step="0.1" value="1.0"> | |
| </div> | |
| </div> | |
| <div class="api-key-section"> | |
| <div class="api-key-group"> | |
| <label for="groq-token">Groq API Key</label> | |
| <input type="password" id="groq-token" placeholder="gsk_..."> | |
| </div> | |
| <div class="api-key-group"> | |
| <label for="hf-token">Hugging Face Token</label> | |
| <input type="password" id="hf-token" placeholder="hf_..."> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const navItems = document.querySelectorAll('.nav-item'); | |
| const promptViews = document.querySelectorAll('.prompt-view'); | |
| const modelList = document.getElementById('model-list'); | |
| const newChatBtn = document.getElementById('new-chat-btn'); | |
| const chatHistoryList = document.getElementById('chat-history-list'); | |
| const chatHistoryDisplay = document.getElementById('chat-history-display'); | |
| const homeScreen = document.getElementById('home-screen'); | |
| const chatView = document.getElementById('chat-view'); | |
| const tempSlider = document.getElementById('temperature'); | |
| const tempValue = document.getElementById('temperature-value'); | |
| const topPSlider = document.getElementById('top-p'); | |
| const topPValue = document.getElementById('top-p-value'); | |
| const hfTokenInput = document.getElementById('hf-token'); | |
| const groqTokenInput = document.getElementById('groq-token'); | |
| const imageUploader = document.getElementById('image-uploader'); | |
| const imagePreview = document.getElementById('image-preview'); | |
| let activeMode = 'text-generation'; | |
| let activeModelInfo = {}; | |
| let temperature = 0.9; | |
| let topP = 1.0; | |
| let uploadedImageBase64 = null; | |
| let uploadedImageDataUrl = null; | |
| let chatSessions = {}; | |
| let activeChatId = null; | |
| let editingMessageId = null; | |
| let currentRequestController = null; | |
| let currentTypingInterval = null; | |
| const taskConfig = { | |
| 'text-generation': { | |
| examples: [ | |
| { title: "Explain quantum computing", prompt: "Explain quantum computing in simple terms." }, | |
| { title: "Python factorial function", prompt: "Write a Python function that calculates the factorial of a number." }, | |
| { title: "Email to boss", prompt: "Write a short, professional email to my boss requesting a meeting next week." } | |
| ], | |
| models: [ | |
| { id: "llama3-8b-8192", name: "Llama3 8B (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true }, | |
| { id: "qwen/qwen3-32b", name: "qwen/qwen3-32b (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true }, | |
| { id: "gemma2-9b-it", name: "gemma2-9b-it (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true }, | |
| { id: "deepseek-ai/DeepSeek-V3", name: "DeepSeek-V3 (HF)", url: "https://router.huggingface.co/nebius/v1/chat/completions", type: "chat" }, | |
| ] | |
| }, | |
| 'image-to-text': { | |
| examples: [ | |
| { title: "Describe scene", prompt: "Describe this scene in detail." }, | |
| { title: "Identify objects", prompt: "List all the objects you can identify." }, | |
| { title: "Suggest a caption", prompt: "Suggest a creative social media caption." } | |
| ], | |
| models: [ | |
| { id: "meta-llama/llama-4-scout-17b-16e-instruct", name: "meta-llama/llama-4-scout (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "image-to-text",isGroq: true }, | |
| { id: "meta-llama/llama-4-maverick-17b-128e-instruct", name: "meta-llama/llama-4-maverick (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "image-to-text",isGroq: true } | |
| ] | |
| }, | |
| 'text-classification': { | |
| examples: [ | |
| { title: "Sentiment analysis", prompt: "I'm so frustrated with customer service!" }, | |
| { title: "Topic identification", prompt: "The new legislation aims to reduce carbon emissions." }, | |
| { title: "Spam detection", prompt: "CLICK HERE to claim your prize!" } | |
| ], | |
| models: [ | |
| { id: "distilbert-base-uncased-finetuned-sst-2-english", name: "DistilBERT SST-2", url: "https://router.huggingface.co/hf-inference/models/distilbert/distilbert-base-uncased-finetuned-sst-2-english", type: "text-classification" }, | |
| { id: "SamLowe/roberta-base-go_emotions", name: "RoBERTa GoEmotions", url: "https://api-inference.huggingface.co/models/SamLowe/roberta-base-go_emotions", type: "text-classification" }, | |
| { id: "nlptown/bert-base-multilingual-uncased-sentiment", name: "BERT Multilingual", url: "https://api-inference.huggingface.co/models/nlptown/bert-base-multilingual-uncased-sentiment", type: "text-classification" } | |
| ] | |
| } | |
| }; | |
| function init() { | |
| populatePromptExamples(); | |
| setupListeners(); | |
| switchMode(activeMode); | |
| updateAllSendButtonStates(); | |
| } | |
| function populatePromptExamples() { | |
| for (const mode in taskConfig) { | |
| const containerId = `${mode}-examples`; | |
| const container = document.getElementById(containerId); | |
| if(container) { | |
| container.innerHTML = ''; | |
| taskConfig[mode].examples.forEach(ex => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'prompt-example-btn'; | |
| btn.textContent = ex.title; | |
| btn.onclick = () => { | |
| const promptTextarea = document.getElementById(`${mode}-prompt`); | |
| promptTextarea.value = ex.prompt; | |
| promptTextarea.focus(); | |
| autoGrowTextarea(promptTextarea); | |
| updateSendButtonState(mode); | |
| }; | |
| container.appendChild(btn); | |
| }); | |
| } | |
| } | |
| } | |
| function setupListeners() { | |
| navItems.forEach(item => item.addEventListener('click', () => switchMode(item.dataset.target))); | |
| newChatBtn.addEventListener('click', startNewChat); | |
| tempSlider.addEventListener('input', (e) => { | |
| temperature = parseFloat(e.target.value); | |
| tempValue.textContent = temperature.toFixed(1); | |
| }); | |
| topPSlider.addEventListener('input', (e) => { | |
| topP = parseFloat(e.target.value); | |
| topPValue.textContent = topP.toFixed(1); | |
| }); | |
| document.querySelectorAll('textarea').forEach(textarea => { | |
| textarea.addEventListener('input', () => { | |
| autoGrowTextarea(textarea); | |
| const mode = textarea.id.replace('-prompt', ''); | |
| updateSendButtonState(mode); | |
| }); | |
| }); | |
| [hfTokenInput, groqTokenInput].forEach(input => { | |
| input.addEventListener('input', () => input.classList.remove('input-error')); | |
| }); | |
| setupImageUploader(); | |
| setupActionButtons(); | |
| } | |
| function updateSendButtonState(mode) { | |
| if (mode === 'image-to-text') return; | |
| const promptTextarea = document.getElementById(`${mode}-prompt`); | |
| const sendButton = document.querySelector(`#${mode} .action-button`); | |
| if (promptTextarea && sendButton) { | |
| sendButton.disabled = promptTextarea.value.trim() === ''; | |
| } | |
| } | |
| function updateAllSendButtonStates() { | |
| ['text-generation', 'text-classification'].forEach(updateSendButtonState); | |
| } | |
| function setupImageUploader() { | |
| const uploaderInput = document.getElementById('image-upload-input'); | |
| imageUploader.addEventListener('click', () => uploaderInput.click()); | |
| imageUploader.addEventListener('dragover', (e) => { e.preventDefault(); imageUploader.style.borderColor = 'var(--accent-color)'; }); | |
| imageUploader.addEventListener('dragleave', (e) => { imageUploader.style.borderColor = 'var(--border-color)'; }); | |
| imageUploader.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| imageUploader.style.borderColor = 'var(--border-color)'; | |
| if (e.dataTransfer.files.length > 0) handleImageFile(e.dataTransfer.files[0]); | |
| }); | |
| uploaderInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) handleImageFile(e.target.files[0]); | |
| }); | |
| } | |
| function autoGrowTextarea(element) { | |
| element.style.height = 'auto'; | |
| element.style.height = (element.scrollHeight) + 'px'; | |
| } | |
| function switchMode(targetMode) { | |
| activeMode = targetMode; | |
| navItems.forEach(i => i.classList.toggle('active', i.dataset.target === targetMode)); | |
| promptViews.forEach(v => { | |
| v.classList.toggle('active', v.id === targetMode) | |
| v.style.display = v.id === targetMode ? 'flex' : 'none'; | |
| }); | |
| renderModelList(targetMode); | |
| if (activeChatId) { | |
| chatSessions[activeChatId].mode = targetMode; | |
| } | |
| } | |
| function renderModelList(mode) { | |
| modelList.innerHTML = ''; | |
| const models = taskConfig[mode].models; | |
| if (!models || models.length === 0) return; | |
| setActiveModel(models[0]); | |
| models.forEach(model => { | |
| const item = document.createElement('li'); | |
| item.className = 'model-item'; | |
| item.textContent = model.name; | |
| item.title = model.id; | |
| item.dataset.modelId = model.id; | |
| if (model.isGroq) { | |
| item.classList.add('groq-model'); | |
| } | |
| if (model.id === activeModelInfo.id) { | |
| item.classList.add('active'); | |
| } | |
| item.onclick = () => { | |
| setActiveModel(model); | |
| document.querySelectorAll('.model-item').forEach(i => i.classList.remove('active')); | |
| item.classList.add('active'); | |
| }; | |
| modelList.appendChild(item); | |
| }); | |
| } | |
| function setActiveModel(model) { | |
| activeModelInfo = model; | |
| } | |
| // MODIFIED: startNewChat now creates a new session container and hides others | |
| function startNewChat() { | |
| editingMessageId = null; | |
| const newChatId = `chat_${Date.now()}`; | |
| activeChatId = newChatId; | |
| chatSessions[newChatId] = { | |
| id: newChatId, | |
| title: "New Chat", | |
| mode: activeMode, | |
| messages: [] | |
| }; | |
| // Hide all other session containers | |
| document.querySelectorAll('.chat-session-container').forEach(c => c.style.display = 'none'); | |
| // Create and append a new container for this chat | |
| const newSessionContainer = document.createElement('div'); | |
| newSessionContainer.className = 'chat-session-container'; | |
| newSessionContainer.id = `session-container-${newChatId}`; | |
| newSessionContainer.style.display = 'flex'; // Make it visible | |
| chatHistoryDisplay.appendChild(newSessionContainer); | |
| renderChatHistory(); | |
| homeScreen.classList.remove('active'); | |
| homeScreen.style.display = 'none'; | |
| chatView.style.display = 'flex'; | |
| switchMode(activeMode); | |
| resetImageUploader(); | |
| updateAllSendButtonStates(); | |
| } | |
| // MODIFIED: loadChat now shows/hides containers instead of clearing the display | |
| function loadChat(chatId) { | |
| editingMessageId = null; | |
| activeChatId = chatId; | |
| const chat = chatSessions[chatId]; | |
| // Hide all session containers | |
| document.querySelectorAll('.chat-session-container').forEach(c => c.style.display = 'none'); | |
| let targetContainer = document.getElementById(`session-container-${chatId}`); | |
| // If the container for this chat doesn't exist, create and populate it | |
| if (!targetContainer) { | |
| targetContainer = document.createElement('div'); | |
| targetContainer.className = 'chat-session-container'; | |
| targetContainer.id = `session-container-${chatId}`; | |
| chat.messages.forEach(msg => { | |
| const messageEl = createMessageElement(msg); // Use helper to create element | |
| if (messageEl) targetContainer.appendChild(messageEl); | |
| }); | |
| chatHistoryDisplay.appendChild(targetContainer); | |
| } | |
| // Show the target container | |
| targetContainer.style.display = 'flex'; | |
| homeScreen.classList.remove('active'); | |
| homeScreen.style.display = 'none'; | |
| chatView.style.display = 'flex'; | |
| switchMode(chat.mode); | |
| renderChatHistory(); | |
| } | |
| function renderChatHistory() { | |
| chatHistoryList.innerHTML = ''; | |
| Object.values(chatSessions).reverse().forEach(chat => { | |
| const item = document.createElement('div'); | |
| item.className = 'history-item'; | |
| item.dataset.chatId = chat.id; | |
| item.innerHTML = ` | |
| <span class="material-icons">chat_bubble</span> | |
| <span class="history-title-text item-text">${chat.title}</span> | |
| <div class="history-actions"> | |
| <span class="material-icons history-icon" onclick="editChatTitle('${chat.id}', event)">edit</span> | |
| <span class="material-icons history-icon" onclick="deleteChat('${chat.id}', event)">delete</span> | |
| </div> | |
| `; | |
| if(chat.id === activeChatId) { | |
| item.classList.add('active'); | |
| } | |
| item.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('history-icon')) return; | |
| loadChat(chat.id) | |
| }); | |
| chatHistoryList.appendChild(item); | |
| }); | |
| } | |
| window.deleteChat = (chatId, event) => { | |
| event.stopPropagation(); | |
| if (confirm(`Are you sure you want to delete this chat?`)) { | |
| delete chatSessions[chatId]; | |
| // Also remove the chat container from the DOM | |
| const chatContainer = document.getElementById(`session-container-${chatId}`); | |
| if (chatContainer) chatContainer.remove(); | |
| if (activeChatId === chatId) { | |
| activeChatId = null; | |
| chatView.style.display = 'none'; | |
| homeScreen.style.display = 'flex'; | |
| homeScreen.classList.add('active'); | |
| } | |
| renderChatHistory(); | |
| } | |
| }; | |
| window.editChatTitle = (chatId, event) => { | |
| event.stopPropagation(); | |
| const item = event.target.closest('.history-item'); | |
| const titleSpan = item.querySelector('.history-title-text'); | |
| const currentTitle = titleSpan.textContent; | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'history-title-input'; | |
| input.value = currentTitle; | |
| titleSpan.replaceWith(input); | |
| input.focus(); | |
| const saveTitle = () => { | |
| const newTitle = input.value.trim(); | |
| if (newTitle && newTitle !== currentTitle) { | |
| chatSessions[chatId].title = newTitle; | |
| } | |
| renderChatHistory(); | |
| }; | |
| input.addEventListener('blur', saveTitle); | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') saveTitle(); | |
| else if (e.key === 'Escape') renderChatHistory(); | |
| }); | |
| }; | |
| function handleImageFile(file) { | |
| if (!file.type.startsWith('image/')) return; | |
| imageUploader.classList.remove('has-image'); | |
| imageUploader.classList.add('loading'); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| uploadedImageDataUrl = e.target.result; | |
| uploadedImageBase64 = uploadedImageDataUrl.split(',')[1]; | |
| imagePreview.src = uploadedImageDataUrl; | |
| imageUploader.classList.remove('loading'); | |
| imageUploader.classList.add('has-image'); | |
| document.querySelector('#image-to-text .action-button').disabled = false; | |
| } | |
| reader.readAsDataURL(file); | |
| } | |
| function resetImageUploader() { | |
| imageUploader.classList.remove('has-image', 'loading'); | |
| imagePreview.src = ''; | |
| uploadedImageDataUrl = null; | |
| uploadedImageBase64 = null; | |
| document.querySelector('#image-to-text .action-button').disabled = true; | |
| } | |
| function addMessageToSession(sender, content, options = {}) { | |
| if (!activeChatId) return null; | |
| const messageId = `msg_${Date.now()}`; | |
| const message = { | |
| id: messageId, | |
| sender, | |
| content, | |
| isHtml: options.isHtml || false, | |
| isError: options.isError || false, | |
| }; | |
| chatSessions[activeChatId].messages.push(message); | |
| return messageId; | |
| } | |
| // ADDED: Refactored function to create a message element without displaying it | |
| function createMessageElement(message, isTyping = false) { | |
| const messageWrapper = document.createElement('div'); | |
| messageWrapper.className = `chat-message-container ${message.sender}`; | |
| messageWrapper.id = message.id; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `chat-message ${message.sender}`; | |
| if(message.isError) { messageDiv.classList.add('error'); } | |
| const avatar = `<div class="chat-avatar"><span class="material-icons">${message.sender === 'user' ? 'person' : 'auto_awesome'}</span></div>`; | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'chat-bubble'; | |
| if (message.isHtml) { | |
| bubble.innerHTML = message.content; | |
| } else { | |
| bubble.textContent = message.content; | |
| } | |
| if (message.sender === 'model' && !isTyping && !message.isError) { | |
| bubble.innerHTML += `<span class="material-icons copy-icon-bubble" onclick="copyResponse(event)" title="Copy response">content_copy</span>`; | |
| } | |
| if (message.sender === 'user') { | |
| messageDiv.innerHTML = bubble.outerHTML + avatar; | |
| } else { | |
| messageDiv.innerHTML = avatar + bubble.outerHTML; | |
| } | |
| messageWrapper.appendChild(messageDiv); | |
| // Add edit/resend bar for user messages | |
| if (message.sender === 'user' && !isTyping && chatSessions[activeChatId]?.mode !== 'image-to-text') { | |
| const actionBar = document.createElement('div'); | |
| actionBar.className = 'prompt-actions-bar'; | |
| actionBar.innerHTML = ` | |
| <span class="action-icon" onclick="editMessage('${message.id}')" title="Edit prompt"> | |
| <span class="material-icons">edit</span> | |
| </span> | |
| `; | |
| messageWrapper.appendChild(actionBar); | |
| } | |
| return messageWrapper; | |
| } | |
| // MODIFIED: displayMessage now finds the active container and appends the element | |
| function displayMessage(message, isTyping = false) { | |
| const activeSessionContainer = document.getElementById(`session-container-${activeChatId}`); | |
| if (!activeSessionContainer) { | |
| console.error('Active session container not found!'); | |
| return null; | |
| } | |
| const messageWrapper = createMessageElement(message, isTyping); | |
| if (messageWrapper) { | |
| activeSessionContainer.appendChild(messageWrapper); | |
| activeSessionContainer.scrollTop = activeSessionContainer.scrollHeight; | |
| } | |
| return messageWrapper; | |
| } | |
| function streamResponse(element, text, mode) { | |
| let i = 0; | |
| element.innerHTML = ''; | |
| currentRequestController = new AbortController(); | |
| const signal = currentRequestController.signal; | |
| toggleSendStopButton(true, true); | |
| const activeSessionContainer = document.getElementById(`session-container-${activeChatId}`); | |
| currentTypingInterval = setInterval(() => { | |
| if (signal.aborted) { | |
| clearInterval(currentTypingInterval); | |
| currentTypingInterval = null; | |
| element.innerHTML += '... [Stopped]'; | |
| toggleSendStopButton(false); | |
| if (mode === 'image-to-text') { | |
| resetImageUploader(); | |
| } | |
| return; | |
| } | |
| if (i < text.length) { | |
| element.innerHTML += text.charAt(i); | |
| i++; | |
| if (activeSessionContainer) { | |
| activeSessionContainer.scrollTop = activeSessionContainer.scrollHeight; | |
| } | |
| } else { | |
| clearInterval(currentTypingInterval); | |
| currentTypingInterval = null; | |
| toggleSendStopButton(false); | |
| if (mode === 'image-to-text') { | |
| resetImageUploader(); | |
| } | |
| } | |
| }, 10); | |
| } | |
| window.editMessage = (messageId) => { | |
| if (!messageId) return; | |
| const chat = chatSessions[activeChatId]; | |
| if (!chat) return; | |
| const message = chat.messages.find(m => m.id === messageId); | |
| if (!message) return; | |
| const promptTextarea = document.getElementById(`${activeMode}-prompt`); | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = message.content; | |
| promptTextarea.value = tempDiv.textContent || ""; | |
| promptTextarea.focus(); | |
| updateSendButtonState(activeMode); | |
| editingMessageId = messageId; | |
| }; | |
| window.resendMessage = (messageId) => { | |
| if (!messageId) return; | |
| const chat = chatSessions[activeChatId]; | |
| if (!chat) return; | |
| const message = chat.messages.find(m => m.id === messageId); | |
| if (!message) return; | |
| displayMessage(message); | |
| const tempDiv = document.createElement('div'); | |
| tempDiv.innerHTML = message.content; | |
| handleAction(null, tempDiv.textContent || ""); | |
| }; | |
| window.copyResponse = (event) => { | |
| const icon = event.target; | |
| const bubble = icon.closest('.chat-bubble'); | |
| if (!bubble) return; | |
| const contentClone = bubble.cloneNode(true); | |
| contentClone.querySelector('.copy-icon-bubble').remove(); | |
| const textToCopy = contentClone.querySelector('.response-content')?.textContent || contentClone.textContent; | |
| navigator.clipboard.writeText(textToCopy).then(() => { | |
| icon.textContent = 'check'; | |
| setTimeout(() => { icon.textContent = 'content_copy'; }, 1500); | |
| }).catch(err => console.error('Failed to copy: ', err)); | |
| }; | |
| function createModelResponseHeader() { | |
| return ` | |
| <div class="model-response-header"> | |
| <span class="material-icons">memory</span> | |
| <span>${activeModelInfo.name}</span> | |
| <span>| Temp: ${temperature.toFixed(1)}</span> | | |
| <span>Top-P: ${topP.toFixed(1)}</span> | |
| </div> | |
| `; | |
| } | |
| function setupActionButtons() { | |
| document.querySelectorAll('.action-button').forEach(btn => { | |
| btn.onclick = handleAction; | |
| }); | |
| } | |
| function toggleSendStopButton(isGenerating, isTyping = false) { | |
| document.querySelectorAll('.action-button').forEach(btn => { | |
| const currentTextarea = document.getElementById(`${btn.dataset.target}-prompt`); | |
| if (isGenerating || isTyping) { | |
| btn.innerHTML = `<span class="material-icons">stop_circle</span>`; | |
| btn.classList.add('stop-generating'); | |
| btn.disabled = false; | |
| if(currentTextarea) currentTextarea.disabled = true; | |
| btn.onclick = () => { | |
| if (currentRequestController) currentRequestController.abort(); | |
| if(currentTypingInterval) clearInterval(currentTypingInterval); | |
| toggleSendStopButton(false); | |
| }; | |
| } else { | |
| btn.innerHTML = `<span class="material-icons">send</span>`; | |
| btn.classList.remove('stop-generating'); | |
| if(currentTextarea) currentTextarea.disabled = false; | |
| updateSendButtonState(btn.dataset.target); | |
| btn.onclick = handleAction; | |
| } | |
| }); | |
| const activeSessionContainer = document.getElementById(`session-container-${activeChatId}`); | |
| if (!activeSessionContainer) return; | |
| const userMessageContainers = activeSessionContainer.querySelectorAll('.chat-message-container.user'); | |
| if (userMessageContainers.length > 0) { | |
| const lastUserMessage = userMessageContainers[userMessageContainers.length - 1]; | |
| const actionBar = lastUserMessage.querySelector('.prompt-actions-bar'); | |
| if (actionBar) { | |
| actionBar.style.display = (isGenerating || isTyping) ? 'none' : 'flex'; | |
| } | |
| } | |
| } | |
| async function handleAction(e, overridePrompt = null) { | |
| if(e) e.preventDefault(); | |
| const targetMode = e ? e.target.closest('.action-button').dataset.target : activeMode; | |
| if (!activeChatId) { | |
| startNewChat(); | |
| } | |
| const isGroqModel = activeModelInfo.isGroq; | |
| const apiKeyInput = isGroqModel ? groqTokenInput : hfTokenInput; | |
| const apiKey = apiKeyInput.value.trim(); | |
| const apiKeyName = isGroqModel ? "Groq API Key" : "Hugging Face API Token"; | |
| if (!apiKey) { | |
| const errorMsg = { id: `err_${Date.now()}`, sender: 'model', content: `Please enter your ${apiKeyName}.`, isError: true }; | |
| displayMessage(errorMsg); | |
| apiKeyInput.classList.add('input-error'); | |
| return; | |
| } | |
| const promptTextarea = document.getElementById(`${targetMode}-prompt`); | |
| let prompt = overridePrompt !== null ? overridePrompt : promptTextarea?.value.trim() || ''; | |
| if (targetMode === 'image-to-text' && !uploadedImageBase64 && !editingMessageId) { | |
| const errorMsg = { id: `err_${Date.now()}`, sender: 'model', content: 'Please upload an image first.', isError: true }; | |
| displayMessage(errorMsg); | |
| return; | |
| } | |
| if (!prompt && targetMode !== 'image-to-text' && overridePrompt === null) return; | |
| let userMessageContent = prompt; | |
| if (targetMode === 'image-to-text') { | |
| userMessageContent = `<img src="${uploadedImageDataUrl}" alt="Uploaded preview" style="max-height: 150px; border-radius: 8px; margin-bottom: 8px;">${prompt ? `<br>${prompt}`: ''}`; | |
| } | |
| if (overridePrompt === null) { | |
| let userMessageId; | |
| if (editingMessageId) { | |
| const messageToUpdate = chatSessions[activeChatId].messages.find(m => m.id === editingMessageId); | |
| if (messageToUpdate) { | |
| messageToUpdate.content = userMessageContent; | |
| // Find the message in the DOM and update it | |
| const messageElement = document.getElementById(editingMessageId); | |
| if(messageElement) { | |
| messageElement.querySelector('.chat-bubble').innerHTML = userMessageContent; | |
| } | |
| userMessageId = editingMessageId; | |
| } | |
| } else { | |
| userMessageId = addMessageToSession('user', userMessageContent, { isHtml: true }); | |
| const userMessage = chatSessions[activeChatId].messages.find(m => m.id === userMessageId); | |
| displayMessage(userMessage); | |
| } | |
| } | |
| if (chatSessions[activeChatId].messages.length <= 2 && prompt && !editingMessageId) { | |
| chatSessions[activeChatId].title = prompt.substring(0, 25) + (prompt.length > 25 ? '...' : ''); | |
| renderChatHistory(); | |
| } | |
| if(promptTextarea) { promptTextarea.value = ''; autoGrowTextarea(promptTextarea); } | |
| updateAllSendButtonStates(); | |
| currentRequestController = new AbortController(); | |
| toggleSendStopButton(true); | |
| const typingMsg = { id: `typing_${Date.now()}`, sender: 'model', content: '...' }; | |
| const typingIndicator = displayMessage(typingMsg, true); | |
| let requestBody; | |
| let headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }; | |
| if (targetMode === 'image-to-text') { | |
| if (isGroqModel) { | |
| const payload = { | |
| model: activeModelInfo.id, | |
| messages: [ { role: "user", content: [ { type: "text", text: prompt }, { type: "image_url", image_url: { url: uploadedImageDataUrl } } ] } ], | |
| temperature, max_tokens: 1024, stream: false | |
| }; | |
| requestBody = JSON.stringify(payload); | |
| } else { | |
| const res = await fetch(uploadedImageDataUrl, { signal: currentRequestController.signal }); | |
| requestBody = await res.blob(); | |
| headers['Content-Type'] = requestBody.type; | |
| } | |
| } else { | |
| let payload; | |
| if (isGroqModel) { | |
| payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP, max_tokens: 1024, stream: false }; | |
| } else if (targetMode === 'text-generation') { | |
| payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP }; | |
| } else { | |
| payload = { inputs: prompt }; | |
| } | |
| requestBody = JSON.stringify(payload); | |
| } | |
| try { | |
| const response = await fetch(activeModelInfo.url, { method: 'POST', headers: headers, body: requestBody, signal: currentRequestController.signal }); | |
| const resultText = await response.text(); | |
| if (!response.ok) { | |
| try { | |
| const errorJson = JSON.parse(resultText); | |
| throw new Error(errorJson.error?.message || errorJson.error || resultText); | |
| } catch(e) { throw new Error(resultText); } | |
| } | |
| let responseContent = ''; | |
| let responseHtml = false; | |
| const finalResponse = JSON.parse(resultText); | |
| switch (targetMode) { | |
| case 'text-generation': | |
| responseContent = finalResponse.choices?.[0]?.message?.content || '[No content]'; | |
| break; | |
| case 'image-to-text': | |
| if (isGroqModel) { | |
| responseContent = finalResponse.choices?.[0]?.message?.content || '[No image caption]'; | |
| } else { | |
| responseContent = finalResponse?.[0]?.generated_text || '[No image caption]'; | |
| } | |
| break; | |
| case 'text-classification': | |
| responseContent = `<pre style="background:var(--bg-color-dark); padding: 12px; border-radius: 8px;">${JSON.stringify(finalResponse, null, 2)}</pre>`; | |
| responseHtml = true; | |
| break; | |
| } | |
| const responseHeader = createModelResponseHeader(); | |
| const fullResponseHtml = responseHeader + `<div class="response-content"></div>`; | |
| if (typingIndicator) typingIndicator.remove(); | |
| const modelMessageId = addMessageToSession('model', fullResponseHtml, { isHtml: true }); | |
| const finalMessage = chatSessions[activeChatId].messages.find(m => m.id === modelMessageId); | |
| const finalMessageEl = displayMessage(finalMessage); | |
| if (finalMessageEl) { | |
| const responseContentEl = finalMessageEl.querySelector('.response-content'); | |
| if (responseHtml) { | |
| responseContentEl.innerHTML = responseContent; | |
| toggleSendStopButton(false); | |
| } else { | |
| streamResponse(responseContentEl, responseContent, targetMode); | |
| } | |
| } | |
| } catch (error) { | |
| if (typingIndicator) { | |
| let errorMessage = `Error: ${error.message}`; | |
| if (error.name === 'AbortError') { | |
| errorMessage = 'Response generation stopped by user.'; | |
| } | |
| typingIndicator.querySelector('.chat-bubble').textContent = errorMessage; | |
| typingIndicator.classList.add('error'); | |
| addMessageToSession('model', errorMessage, { isError: true }); | |
| } | |
| toggleSendStopButton(false); | |
| } finally { | |
| editingMessageId = null; | |
| currentRequestController = null; | |
| } | |
| } | |
| init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| components.html( | |
| html_code, | |
| ) |