at41rv-ai-api / playground.html
At41rv
Create playground.html
393e0fd verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playground - At41rvAI</title>
<link rel="icon" type="image/png" href="/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="shortcut icon" href="/apple-touch-icon.png">
<style>
/* General Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #000000;
color: #ffffff;
line-height: 1.6;
overflow-x: hidden;
}
/* Glassmorphism Effect */
.glass {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
.glass-strong {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
}
/* Navigation */
nav {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
padding: 16px 32px;
max-width: 800px;
width: 90%;
border-radius: 16px;
/* Ensure rounded corners on nav */
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: 800;
color: #ffffff;
}
.nav-links {
display: flex;
gap: 24px;
list-style: none;
}
.nav-links a {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
padding: 8px 16px;
border-radius: 12px;
}
.nav-links a:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.1);
}
/* Main Layout */
.main-container {
display: flex;
min-height: 100vh;
padding-top: 100px;
/* Space for fixed nav */
padding-bottom: 20px;
/* Bottom padding for content */
}
/* Sidebar (Modified to be minimal for Chatbot only) */
.sidebar {
width: 350px;
padding: 20px;
position: fixed;
left: 0;
top: 100px;
bottom: 0;
overflow-y: auto;
z-index: 100;
}
.sidebar-content {
padding: 24px;
height: 100%;
}
.sidebar h2 {
font-size: 1.5rem;
margin-bottom: 20px;
color: #ffffff;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.sidebar-close-btn {
display: none;
/* Hidden by default on larger screens */
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
line-height: 1;
transition: all 0.3s ease;
justify-content: center;
/* Center content */
align-items: center;
/* Center content */
}
.sidebar-close-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
@media (max-width: 1024px) {
.sidebar-close-btn {
display: flex;
/* Show on smaller screens */
}
}
.endpoint-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.endpoint-item {
padding: 16px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.endpoint-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(4px);
}
.endpoint-item.active {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.endpoint-method {
font-size: 0.75rem;
font-weight: 700;
padding: 4px 8px;
border-radius: 6px;
background: #ffffff;
color: #000000;
display: inline-block;
margin-bottom: 8px;
}
.endpoint-name {
font-weight: 600;
margin-bottom: 4px;
}
.endpoint-desc {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.7);
}
/* Main Content */
.content {
flex: 1;
margin-left: 350px;
/* Space for fixed sidebar */
padding: 20px;
}
.playground-header {
text-align: center;
margin-bottom: 40px;
}
.playground-title {
font-size: 3rem;
font-weight: 800;
margin-bottom: 16px;
background: linear-gradient(135deg, #ffffff 0%, rgba(255, 255, 255, 0.7) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.playground-subtitle {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.7);
}
/* Playground Interface */
.playground-interface {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1200px;
margin: 0 auto;
}
.input-panel,
.output-panel {
padding: 24px;
min-height: 500px;
}
.panel-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
font-size: 14px;
transition: all 0.3s ease;
font-family: inherit;
/* Inherit font for inputs */
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.1);
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.btn-primary {
background: #ffffff;
color: #000000;
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.9);
transform: translateY(-2px);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.output-content {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 16px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 14px;
white-space: pre-wrap;
overflow-y: auto;
max-height: calc(100vh - 300px);
/* Adjust based on viewport height */
}
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: rgba(255, 255, 255, 0.7);
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.error {
color: #ff6b6b;
background: rgba(255, 107, 107, 0.1);
border: 1px solid rgba(255, 107, 107, 0.3);
padding: 12px;
border-radius: 8px;
margin-top: 12px;
}
.success {
color: #51cf66;
background: rgba(81, 207, 102, 0.1);
border: 1px solid rgba(81, 207, 102, 0.3);
padding: 12px;
border-radius: 8px;
margin-top: 12px;
}
/* Responsive */
@media (max-width: 1024px) {
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 320px;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
background: rgba(0, 0, 0, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0;
}
.sidebar.open {
transform: translateX(0);
}
.content {
margin-left: 0;
padding: 20px 15px;
width: 100%;
/* Take full width on mobile */
}
.playground-interface {
grid-template-columns: 1fr;
gap: 20px;
}
.mobile-menu-btn {
display: flex;
align-items: center;
gap: 8px;
position: fixed;
top: 100px;
left: 20px;
z-index: 1002;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #ffffff;
padding: 12px 16px;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.mobile-menu-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.hamburger-icon {
transition: transform 0.3s ease;
display: inline-block;
}
.mobile-menu-btn.active .hamburger-icon {
transform: rotate(90deg);
}
}
@media (max-width: 768px) {
.nav-links {
display: none;
/* Hide navigation links on smaller screens */
}
.nav-container {
justify-content: center;
/* Center logo when links are hidden */
}
.playground-title {
font-size: 2.5rem;
}
.playground-subtitle {
font-size: 1.125rem;
}
.input-panel,
.output-panel {
padding: 20px 16px;
}
.sidebar {
width: 300px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.sidebar-content {
padding: 24px 20px;
height: auto;
min-height: 100vh;
padding-top: 120px;
/* Account for nav height */
}
.endpoint-item {
padding: 14px 12px;
margin-bottom: 8px;
}
.endpoint-name {
font-size: 15px;
font-weight: 600;
}
.endpoint-desc {
font-size: 13px;
margin-top: 4px;
}
.endpoint-method {
font-size: 11px;
padding: 3px 6px;
}
}
@media (max-width: 480px) {
nav {
width: 95%;
padding: 12px 16px;
}
.logo {
font-size: 18px;
}
.playground-title {
font-size: 2rem;
}
.playground-subtitle {
font-size: 1rem;
}
.input-panel,
.output-panel {
padding: 16px 12px;
}
.form-group {
margin-bottom: 16px;
}
.form-input,
.form-select,
.form-textarea {
padding: 10px 12px;
font-size: 16px;
/* Prevents zoom on iOS */
}
.btn {
padding: 12px 20px;
font-size: 14px;
width: 100%;
}
.sidebar {
width: 260px;
}
.sidebar-content {
padding: 20px 16px;
}
.endpoint-list {
gap: 8px;
}
.endpoint-item {
padding: 10px;
}
.mobile-menu-btn {
top: 110px;
left: 15px;
padding: 10px;
}
}
.mobile-menu-btn {
display: none;
/* Default to hidden unless @media rule applies */
}
/* Ensure mobile menu is always visible on small screens */
@media (max-width: 1024px) {
.mobile-menu-btn {
display: flex !important;
}
}
/* Mobile backdrop */
.mobile-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}
.mobile-backdrop.show {
display: block;
opacity: 1;
}
@media (max-width: 1024px) {
.mobile-backdrop.show {
display: block;
}
}
</style>
</head>
<body>
<nav class="glass">
<div class="nav-container">
<div class="logo">At41rvAI</div>
<ul class="nav-links">
<li><a href='/'>Home</a></li>
<li><a href="/playground">Playground</a></li>
</ul>
</div>
</nav>
<button class="mobile-menu-btn" onclick="toggleSidebar()" id="mobile-menu-btn">
<span class="hamburger-icon"></span> Menu
</button>
<div class="mobile-backdrop" id="mobile-backdrop" onclick="closeSidebar()"></div>
<div class="main-container">
<div class="sidebar glass" id="sidebar">
<div class="sidebar-content">
<div class="sidebar-header">
<h2>API Endpoints</h2>
<button class="sidebar-close-btn" onclick="closeSidebar()"></button>
</div>
<div class="endpoint-list">
<div class="endpoint-item active" onclick="selectEndpoint('chat')">
<div class="endpoint-method">POST</div>
<div class="endpoint-name">Chat Completions</div>
<div class="endpoint-desc">Generate AI responses</div>
</div>
</div>
</div>
</div>
<div class="content">
<div class="playground-header">
<h1 class="playground-title">At41rvAI Playground</h1>
<p class="playground-subtitle">Interact with your intelligent chat companion</p>
</div>
<div class="playground-interface">
<div class="input-panel glass">
<h3 class="panel-title">
<span>⚙️</span> Request
</h3>
<div id="chat-form" class="endpoint-form">
<div class="form-group">
<label class="form-label">Model</label>
<select class="form-select" id="chat-model">
<option value="">Loading models...</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Prompt</label>
<textarea class="form-textarea" id="chat-prompt" placeholder="Enter your message here...">Hello At41rvAI! How are you today?</textarea>
</div>
<button class="btn btn-primary" onclick="testChatCompletion()">Send Request</button>
</div>
</div>
<div class="output-panel glass">
<h3 class="panel-title">
<span>📤</span> Response
</h3>
<div class="output-content" id="output">
Send a request to see the AI's response here.
</div>
</div>
</div>
</div>
</div>
<script>
// API Configuration
// IMPORTANT: This should be the direct URL to your NEW, PRIVATE proxy Space.
// Format: https://<username>-<proxy-space-name>.hf.space
const PROXY_API_BASE_URL = 'https://at41rv-ai-api-proxy.hf.space';
// Sidebar and Mobile Menu Functions (unchanged, not related to API calls)
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('mobile-backdrop');
const menuBtn = document.getElementById('mobile-menu-btn');
sidebar.classList.toggle('open');
backdrop.classList.toggle('show');
menuBtn.classList.toggle('active');
if (sidebar.classList.contains('open')) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
}
function closeSidebar() {
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('mobile-backdrop');
const menuBtn = document.getElementById('mobile-menu-btn');
sidebar.classList.remove('open');
backdrop.classList.remove('show');
menuBtn.classList.remove('active');
document.body.style.overflow = '';
}
function selectEndpoint(endpoint) {
document.querySelectorAll('.endpoint-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`.endpoint-item`).classList.add('active');
document.querySelectorAll('.endpoint-form').forEach(form => {
form.style.display = 'none';
});
document.getElementById(endpoint + '-form').style.display = 'block';
document.getElementById('output').textContent = 'Send a request to see the AI\'s response here.';
}
// UI Feedback Functions (unchanged, visual feedback)
function showLoading() {
document.getElementById('output').innerHTML = `
<div class="loading">
<div class="spinner"></div>
<span>At41rvAI is thinking...</span>
</div>
`;
}
function showError(message) {
document.getElementById('output').innerHTML = `
<div class="error">
<strong>Error:</strong> ${message}
</div>
`;
}
function showSuccess(data) {
// Updated to handle the nested structure of the AI response
let aiResponseContent = "No content received.";
if (data && data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) {
aiResponseContent = data.choices[0].message.content;
} else {
aiResponseContent = JSON.stringify(data, null, 2); // Fallback to full JSON if content not found
}
document.getElementById('output').innerHTML = `
<div class="success">
<strong>✅ Success!</strong>
</div>
<pre style="background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 12px; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; color: rgba(255, 255, 255, 0.9); white-space: pre-wrap; overflow-x: auto; max-height: 200px; overflow-y: auto;">${aiResponseContent}</pre>
`;
}
// Load chat models (unchanged, hardcoded for consistency)
async function loadChatModels() {
const chatModelSelect = document.getElementById('chat-model');
chatModelSelect.innerHTML = '';
const chatGPTModels = ['gpt-4o', 'gpt-4o-mini'];
const optgroup = document.createElement('optgroup');
optgroup.label = 'ChatGPT Models';
chatGPTModels.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
optgroup.appendChild(option);
});
chatModelSelect.appendChild(optgroup);
chatModelSelect.value = 'gpt-4o'; // Set default
console.log('ChatGPT models loaded:', chatGPTModels);
}
// Function to test Chat Completion - Calls the new proxy endpoint
async function testChatCompletion() {
showLoading();
const model = document.getElementById('chat-model').value;
const prompt = document.getElementById('chat-prompt').value;
if (!model) {
showError('Please select a model.');
return;
}
if (!prompt.trim()) {
showError('Please enter a prompt.');
return;
}
try {
// Call your new proxy Space's /proxy-chat endpoint
const response = await fetch(`${PROXY_API_BASE_URL}/proxy-chat`, { // <--- THIS IS THE CRUCIAL CHANGE
method: 'POST',
headers: {
'Content-Type': 'application/json',
// NO API KEY IS SENT FROM THE CLIENT-SIDE HERE.
// The proxy handles the secure API_KEY.
},
body: JSON.stringify({
messages: [{
role: "user",
content: prompt
}],
model: model
})
});
const data = await response.json();
if (response.ok) {
showSuccess(data);
} else {
// Assuming the proxy returns error details in 'detail' field
showError(data.detail || `Request failed with status: ${response.status}`);
}
} catch (error) {
showError(`Network Error: ${error.message}. Please check console for more details.`);
console.error('Fetch error:', error);
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
loadChatModels();
selectEndpoint('chat');
});
</script>
</body>
</html>