nlp-NS / app.py
danielle2003's picture
Update app.py
c6cb200 verified
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> &nbsp;
<span>| &nbsp; Temp: ${temperature.toFixed(1)}</span> &nbsp; | &nbsp;
<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,
)