nlp-NS / app.py
danielle2003's picture
Update app.py
79e621b verified
raw
history blame
49.7 kB
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:99vh !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;
}
body {
height: 100vh;
width: 100%;
margin: 0;
padding: 0;
background: var(--bg-color-dark);
font-family: 'Inter', sans-serif;
color: var(--text-color-primary);
}
/* Main Container */
.studio-container {
width: 100%;
height: 100vh;
background-color: rgba(22, 27, 34, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid var(--border-color);
overflow: hidden;
display: flex;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
/* Sidebar Navigation */
.sidebar {
width: 240px;
background-color: rgba(13, 17, 23, 0.9);
padding: 24px;
border-right: 1px solid var(--border-color);
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 24px;
}
.sidebar-header .logo {
font-size: 28px;
color: var(--accent-color);
text-shadow: 0 0 10px var(--accent-glow);
}
.sidebar-header h1 {
font-size: 20px;
margin: 0;
color:white !important;
font-weight: 600;
}
/* UPDATED: New Chat Button Style & Position */
.new-chat-btn {
background: linear-gradient(135deg, #58a6ff, #3a8dff);
border: none;
color: #ffffff;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
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;
}
.nav-menu .nav-item:hover .material-icons { color: var(--text-color-primary); }
.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;
}
.chat-history-list .history-item {
padding: 10px;
font-size: 14px;
border-radius: 6px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
color: var(--text-color-secondary);
}
.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;
}
/* UPDATED: API Key Section Styling */
.api-key-section {
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--bg-color-medium);
}
.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);
}
/* Main Content Area */
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.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 */
#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; }
/* Chat History */
.chat-history-display {
flex-grow: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.chat-message {
display: flex;
gap: 16px;
align-items: flex-start;
position: relative; /* For action buttons */
}
.chat-message.user { justify-content: flex-end; }
.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: 80%;
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;
}
/* UPDATED: Model Response Header & Bubble */
.model-response-header {
font-size: 11px;
color: var(--text-color-secondary);
margin-bottom: 8px;
display: flex;
gap: 8px;
align-items: center;
opacity: 0.7;
}
.model-response-header .material-icons { font-size: 14px; }
.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);
}
/* Styling for code blocks inside bubbles */
.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;
}
/* ADDED: User Message Actions */
.message-actions {
position: absolute;
top: 50%;
left: -70px; /* Adjust as needed */
transform: translateY(-50%);
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.chat-message.user:hover .message-actions {
opacity: 1;
}
.action-icon {
cursor: pointer;
color: var(--text-color-secondary);
background-color: var(--bg-color-medium);
border-radius: 50%;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.action-icon:hover {
color: var(--accent-color);
}
.action-icon .material-icons {
font-size: 18px;
}
/* Prompt Input Area */
.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); } }
/* Models and Parameters Panel */
.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;
}
.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;
}
@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;
}
.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">
<!-- Sidebar -->
<aside class="sidebar">
<div>
<div class="sidebar-header">
<span class="material-icons logo">auto_awesome</span><h1>LLM Studio</h1>
</div>
<button class="new-chat-btn" id="new-chat-btn"><span class="material-icons">add_circle</span>New Chat</button>
<nav class="nav-menu">
<div class="nav-item active" data-target="text-generation"><span class="material-icons">text_fields</span><span>Text Generation</span></div>
<div class="nav-item" data-target="image-to-text"><span class="material-icons">image</span><span>Image to Text</span></div>
<div class="nav-item" data-target="text-classification"><span class="material-icons">label</span><span>Text Classification</span></div>
</nav>
<div class="history-section">
<h3 class="history-title">History</h3>
<div class="chat-history-list" id="chat-history-list">
<!-- Chat history items will be dynamically inserted here -->
</div>
</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>
<!-- Main Content -->
<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">
<!-- Text Generation View -->
<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>
<!-- Image to Text View -->
<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>
<!-- Text Classification View -->
<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>
<!-- Right Panel -->
<aside class="right-panel">
<div>
<p class="panel-header">MODELS</p>
<ul id="model-list">
<!-- Models will be dynamically inserted here -->
</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>
</aside>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const navItems = document.querySelectorAll('.nav-item');
const promptViews = document.querySelectorAll('.prompt-view');
const modelList = document.getElementById('model-list');
const actionButtons = document.querySelectorAll('.action-button');
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');
// App State
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; // To track which message is being edited
// UPDATED: taskConfig with Groq models
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);
}
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);
};
container.appendChild(btn);
});
}
}
}
function setupListeners() {
navItems.forEach(item => item.addEventListener('click', () => switchMode(item.dataset.target)));
actionButtons.forEach(btn => btn.addEventListener('click', handleAction));
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));
});
setupImageUploader();
}
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;
}
function startNewChat() {
editingMessageId = null;
const newChatId = `chat_${Date.now()}`;
activeChatId = newChatId;
chatSessions[newChatId] = {
id: newChatId,
title: "New Chat",
mode: activeMode,
messages: []
};
chatHistoryDisplay.innerHTML = '';
renderChatHistory();
homeScreen.classList.remove('active');
homeScreen.style.display = 'none';
chatView.style.display = 'flex';
switchMode(activeMode);
resetImageUploader();
}
function loadChat(chatId) {
editingMessageId = null;
activeChatId = chatId;
const chat = chatSessions[chatId];
chatHistoryDisplay.innerHTML = '';
chat.messages.forEach(msg => {
displayMessage(msg.id, msg.sender, msg.content, msg.isHtml, msg.isError);
});
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.textContent = chat.title;
item.dataset.chatId = chat.id;
if(chat.id === activeChatId) {
item.classList.add('active');
}
item.onclick = () => loadChat(chat.id);
chatHistoryList.appendChild(item);
});
}
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, isHtml = false, isError = false) {
if (!activeChatId) return;
const messageId = `msg_${Date.now()}`;
const message = { id: messageId, sender, content, isHtml, isError };
chatSessions[activeChatId].messages.push(message);
return messageId;
}
// UPDATED: displayMessage to include edit/resend and message IDs
function displayMessage(messageId, sender, content, isHtml = false, isError = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${sender}`;
messageDiv.id = messageId;
if(isError) { messageDiv.classList.add('error'); }
const avatar = `<div class="chat-avatar"><span class="material-icons">${sender === 'user' ? 'person' : 'auto_awesome'}</span></div>`;
let messageContentHtml = '';
if (sender === 'user') {
// Add action buttons for user messages
const actions = `
<div class="message-actions">
<div class="action-icon" onclick="editMessage('${messageId}')" title="Edit"><span class="material-icons">edit</span></div>
<div class="action-icon" onclick="resendMessage('${messageId}')" title="Resend"><span class="material-icons">replay</span></div>
</div>`;
messageContentHtml += actions;
}
const bubble = document.createElement('div');
bubble.className = 'chat-bubble';
if (isHtml) {
bubble.innerHTML = content;
} else {
bubble.textContent = content;
}
messageContentHtml += bubble.outerHTML;
if (sender === 'user') {
messageDiv.innerHTML = messageContentHtml + avatar;
} else {
messageDiv.innerHTML = avatar + messageContentHtml;
}
chatHistoryDisplay.appendChild(messageDiv);
chatHistoryDisplay.scrollTop = chatHistoryDisplay.scrollHeight;
return messageDiv;
}
// ADDED: streamResponse for typing effect
function streamResponse(element, text, speed = 10) {
let i = 0;
element.innerHTML = ''; // Clear the '...'
const interval = setInterval(() => {
if (i < text.length) {
element.innerHTML += text.charAt(i);
i++;
chatHistoryDisplay.scrollTop = chatHistoryDisplay.scrollHeight;
} else {
clearInterval(interval);
}
}, speed);
}
window.editMessage = (messageId) => {
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`);
// We need to strip HTML for editing. A simple way for this app:
const tempDiv = document.createElement('div');
tempDiv.innerHTML = message.content;
promptTextarea.value = tempDiv.textContent || "";
promptTextarea.focus();
editingMessageId = messageId; // Set the message to be replaced
};
window.resendMessage = (messageId) => {
const chat = chatSessions[activeChatId];
if (!chat) return;
const message = chat.messages.find(m => m.id === messageId);
if (!message) return;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = message.content;
handleAction(null, tempDiv.textContent || ""); // Resend the original text content
};
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>
`;
}
// UPDATED: handleAction to support editing and resending
async function handleAction(e, overridePrompt = null) {
const targetMode = e ? e.currentTarget.dataset.target : activeMode;
if (activeMode !== targetMode) return;
if (!activeChatId) { startNewChat(); }
const isGroqModel = activeModelInfo.isGroq;
const apiKey = isGroqModel ? groqTokenInput.value.trim() : hfTokenInput.value.trim();
const apiKeyName = isGroqModel ? "Groq API Key" : "Hugging Face API Token";
if (!apiKey) {
displayMessage(null, 'model', `Please enter your ${apiKeyName}.`, false, true);
return;
}
const promptTextarea = document.getElementById(`${targetMode}-prompt`);
let prompt = overridePrompt !== null ? overridePrompt : promptTextarea?.value.trim() || '';
if (targetMode === 'image-to-text' && !uploadedImageBase64 && !editingMessageId) {
displayMessage(null, 'model', 'Please upload an image first.', false, true); return;
}
if (!prompt && targetMode !== 'image-to-text') 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 (editingMessageId) {
// Find and update the existing message
const messageToUpdate = chatSessions[activeChatId].messages.find(m => m.id === editingMessageId);
if (messageToUpdate) {
messageToUpdate.content = userMessageContent;
document.getElementById(editingMessageId).querySelector('.chat-bubble').innerHTML = userMessageContent;
}
} else {
addMessageToSession('user', userMessageContent, true);
displayMessage(`msg_${Date.now()}`, 'user', userMessageContent, true);
}
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); }
const typingIndicator = displayMessage(`msg_${Date.now()}`, 'model', '...');
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 {
// Fallback to HF blob-based logic
const res = await fetch(uploadedImageDataUrl);
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 { // text-classification
payload = { inputs: prompt };
}
requestBody = JSON.stringify(payload);
}
try {
const response = await fetch(activeModelInfo.url, { method: 'POST', headers: headers, body: requestBody });
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 = '';
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) {
// Groq returns choices array, just like OpenAI
responseContent = finalResponse.choices?.[0]?.message?.content || '[No image caption]';
} else {
// Hugging Face image-to-text returns an array
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>`;
await fetch("http://localhost:5001/log", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt,
model: activeModelInfo.id,
response: fullResponseHtml
})
});
// Update typing indicator with the header and prepare for streaming
typingIndicator.querySelector('.chat-bubble').innerHTML = fullResponseHtml;
const responseContentEl = typingIndicator.querySelector('.response-content');
if (targetMode === 'text-classification') {
responseContentEl.innerHTML = responseContent; // No streaming for formatted JSON
} else {
streamResponse(responseContentEl, responseContent);
}
addMessageToSession('model', fullResponseHtml, true);
} catch (error) {
const errorMessage = `Error: ${error.message}`;
typingIndicator.querySelector('.chat-bubble').textContent = errorMessage;
typingIndicator.classList.add('error');
addMessageToSession('model', errorMessage, false, true);
} finally {
if (targetMode === 'image-to-text') { resetImageUploader(); }
editingMessageId = null; // Clear editing state
}
}
init();
});
</script>
</body>
</html>
"""
# --- Option 1: Using st.components.v1.html (Recommended for more control) ---
# This allows more control over height and scrolling
components.html(
html_code,
)
# --- Option 2: Using st.markdown with an iframe (Simpler, but less control) ---
# st.markdown(
# f"""
# <iframe src="{REACT_APP_URL}" width="100%" height="800" style="border:none;"></iframe>
# """,
# unsafe_allow_html=True
# )