Spaces:
Running
Running
David Ko
build(frontend): copy flattened CRA build to backend static (fix 404 for /static/js/*); translate UI to English
1fb1877
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>OpenAI Chat UI</title> | |
<style> | |
:root { color-scheme: light dark; } | |
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji', 'Segoe UI Emoji'; margin: 0; padding: 20px; } | |
.container { max-width: 960px; margin: 0 auto; } | |
h1 { font-size: 1.6rem; margin: 0 0 1rem; } | |
.card { border: 1px solid #4443; border-radius: 12px; padding: 16px; margin: 12px 0; background: #00000008; } | |
label { display: block; margin: 8px 0 4px; font-weight: 600; } | |
input[type="text"], input[type="password"], textarea, select { width: 100%; padding: 10px; border-radius: 8px; border: 1px solid #6664; background: transparent; color: inherit; } | |
textarea { min-height: 120px; resize: vertical; } | |
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } | |
.actions { display: flex; gap: 8px; margin-top: 12px; } | |
button { padding: 10px 16px; border-radius: 8px; border: 1px solid #4446; background: #2d6cdf; color: #fff; cursor: pointer; } | |
button.secondary { background: #666; } | |
.log { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; } | |
.muted { opacity: .8; font-size: .9rem; } | |
.header { display:flex; align-items:center; justify-content: space-between; gap: 8px; } | |
a { color: #2d6cdf; text-decoration: none; } | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>OpenAI Chat</h1> | |
<div> | |
<a href="/index.html">Home</a> | |
</div> | |
</div> | |
<div class="card"> | |
<div class="row"> | |
<div> | |
<label for="model">Model</label> | |
<input id="model" type="text" value="gpt-4o-mini" /> | |
</div> | |
<div> | |
<label for="apiKey">OpenAI API Key (optional)</label> | |
<input id="apiKey" type="password" placeholder="sk-... (OPENAI_API_KEY env var supported)" /> | |
</div> | |
</div> | |
<label for="system">System Prompt (optional)</label> | |
<input id="system" type="text" placeholder="You are a helpful assistant." /> | |
<label for="prompt">User Question</label> | |
<textarea id="prompt" placeholder="Type your question here"></textarea> | |
<div class="actions"> | |
<button id="sendBtn">Send Question</button> | |
<button class="secondary" id="clearBtn">Clear</button> | |
</div> | |
</div> | |
<div class="card"> | |
<div class="muted">Response</div> | |
<div id="response" class="log"></div> | |
</div> | |
</div> | |
<script> | |
const $ = (id) => document.getElementById(id); | |
const responseEl = $('response'); | |
function setLoading(on) { | |
$('sendBtn').disabled = on; | |
$('sendBtn').textContent = on ? 'Sending...' : 'Send Question'; | |
} | |
$('clearBtn').addEventListener('click', () => { | |
$('prompt').value = ''; | |
responseEl.textContent = ''; | |
}); | |
$('sendBtn').addEventListener('click', async () => { | |
const prompt = $('prompt').value.trim(); | |
const model = $('model').value.trim() || 'gpt-4o-mini'; | |
const api_key = $('apiKey').value.trim(); | |
const system = $('system').value.trim(); | |
if (!prompt) { | |
alert('Please enter a question.'); | |
return; | |
} | |
setLoading(true); | |
responseEl.textContent = ''; | |
try { | |
const res = await fetch('/api/openai/chat', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
credentials: 'include', | |
body: JSON.stringify({ prompt, model, api_key: api_key || undefined, system: system || undefined }) | |
}); | |
if (!res.ok) { | |
let errTxt = await res.text(); | |
try { errTxt = JSON.stringify(JSON.parse(errTxt), null, 2); } catch {} | |
throw new Error(errTxt); | |
} | |
const data = await res.json(); | |
const { response, model: usedModel, usage, latency_sec } = data; | |
const meta = `Model: ${usedModel} | Latency: ${latency_sec}s` + (usage ? ` | Usage: ${JSON.stringify(usage)}` : ''); | |
responseEl.textContent = response ? (response + '\n\n---\n' + meta) : '(Empty response)\n' + meta; | |
} catch (e) { | |
responseEl.textContent = 'Error: ' + e.message; | |
} finally { | |
setLoading(false); | |
} | |
}); | |
</script> | |
</body> | |
</html> | |