feat(ui): remove api key handling and small ui adjustments (#4948)
Some checks failed
Explorer deployment / build-linux (push) Waiting to run
GPU tests / ubuntu-latest (1.21.x) (push) Waiting to run
generate and publish intel docker caches / generate_caches (intel/oneapi-basekit:2025.0.0-0-devel-ubuntu22.04, linux/amd64, ubuntu-latest) (push) Waiting to run
build container images / hipblas-jobs (-aio-gpu-hipblas, rocm/dev-ubuntu-22.04:6.1, hipblas, true, ubuntu:22.04, extras, latest-gpu-hipblas, latest-aio-gpu-hipblas, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -hipblas-ffmpeg) (push) Waiting to run
build container images / hipblas-jobs (rocm/dev-ubuntu-22.04:6.1, hipblas, false, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -hipblas-core) (push) Waiting to run
build container images / hipblas-jobs (rocm/dev-ubuntu-22.04:6.1, hipblas, false, ubuntu:22.04, extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -hipblas) (push) Waiting to run
build container images / hipblas-jobs (rocm/dev-ubuntu-22.04:6.1, hipblas, true, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -hipblas-ffmpeg-core) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-intel-f16, quay.io/go-skynet/intel-oneapi-base:latest, sycl_f16, true, ubuntu:22.04, extras, latest-gpu-intel-f16, latest-aio-gpu-intel-f16, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -sycl-f16-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-intel-f32, quay.io/go-skynet/intel-oneapi-base:latest, sycl_f32, true, ubuntu:22.04, extras, latest-gpu-intel-f32, latest-aio-gpu-intel-f32, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -sycl-f32-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-nvidia-cuda-11, ubuntu:22.04, cublas, 11, 7, true, extras, latest-gpu-nvidia-cuda-11, latest-aio-gpu-nvidia-cuda-11, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -cublas-cuda11-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (-aio-gpu-nvidia-cuda-12, ubuntu:22.04, cublas, 12, 0, true, extras, latest-gpu-nvidia-cuda-12, latest-aio-gpu-nvidia-cuda-12, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -cublas-cuda12-ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f16, false, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f16-core) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f16, true, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f16-ffmpeg-core) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f32, false, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f32-core) (push) Waiting to run
build container images / self-hosted-jobs (quay.io/go-skynet/intel-oneapi-base:latest, sycl_f32, true, ubuntu:22.04, core, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -sycl-f32-ffmpeg-core) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, , , extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, ) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, , true, extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, auto, -ffmpeg) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, cublas, 11, 7, , extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -cublas-cuda11) (push) Waiting to run
build container images / self-hosted-jobs (ubuntu:22.04, cublas, 12, 0, , extras, --jobs=3 --output-sync=target, linux/amd64, arc-runner-set, false, -cublas-cuda12) (push) Waiting to run
build container images / core-image-build (-aio-cpu, ubuntu:22.04, , true, core, latest-cpu, latest-aio-cpu, --jobs=4 --output-sync=target, linux/amd64,linux/arm64, arc-runner-set, false, auto, -ffmpeg-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 11, 7, , core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda11-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 11, 7, true, core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda11-ffmpeg-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 12, 0, , core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda12-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 12, 0, true, core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -cublas-cuda12-ffmpeg-core) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, vulkan, true, core, latest-vulkan-ffmpeg-core, --jobs=4 --output-sync=target, linux/amd64, arc-runner-set, false, false, -vulkan-ffmpeg-core) (push) Waiting to run
build container images / gh-runner (nvcr.io/nvidia/l4t-jetpack:r36.4.0, cublas, 12, 0, true, core, latest-nvidia-l4t-arm64-core, --jobs=4 --output-sync=target, linux/arm64, ubuntu-24.04-arm, true, false, -nvidia-l4t-arm64-core) (push) Waiting to run
Security Scan / tests (push) Waiting to run
Tests extras backends / tests-transformers (push) Waiting to run
Tests extras backends / tests-rerankers (push) Waiting to run
Tests extras backends / tests-diffusers (push) Waiting to run
Tests extras backends / tests-coqui (push) Waiting to run
tests / tests-linux (1.21.x) (push) Waiting to run
tests / tests-aio-container (push) Waiting to run
tests / tests-apple (1.21.x) (push) Waiting to run
generate and publish GRPC docker caches / generate_caches (ubuntu:22.04, linux/amd64,linux/arm64, arc-runner-set) (push) Has been cancelled

* chore(ui): drop set api key button

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* chore(ui): shore in-progress installs in model view

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(ui): improve text to image view

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto 2025-03-05 19:37:36 +01:00 committed by GitHub
parent 06eb7e9fa7
commit de81b42b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 25 additions and 282 deletions

View File

@ -42,12 +42,6 @@ function toggleLoader(show) {
}
}
function submitKey(event) {
event.preventDefault();
localStorage.setItem("key", document.getElementById("apiKey").value);
document.getElementById("apiKey").blur();
}
function submitSystemPrompt(event) {
event.preventDefault();
localStorage.setItem("system_prompt", document.getElementById("systemPrompt").value);
@ -62,10 +56,9 @@ function submitPrompt(event) {
const input = document.getElementById("input").value;
Alpine.store("chat").add("user", input, image);
document.getElementById("input").value = "";
const key = localStorage.getItem("key");
const systemPrompt = localStorage.getItem("system_prompt");
Alpine.nextTick(() => { document.getElementById('messages').scrollIntoView(false); });
promptGPT(systemPrompt, key, input);
promptGPT(systemPrompt, input);
}
function readInputImage() {
@ -82,7 +75,7 @@ function readInputImage() {
}
async function promptGPT(systemPrompt, key, input) {
async function promptGPT(systemPrompt, input) {
const model = document.getElementById("chat-model").value;
// Set class "loader" to the element with "loader" id
//document.getElementById("loader").classList.add("loader");
@ -160,7 +153,6 @@ function readInputImage() {
const response = await fetch("v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
@ -266,20 +258,12 @@ function readInputImage() {
document.getElementById("input").focus();
}
document.getElementById("key").addEventListener("submit", submitKey);
document.getElementById("system_prompt").addEventListener("submit", submitSystemPrompt);
document.getElementById("prompt").addEventListener("submit", submitPrompt);
document.getElementById("input").focus();
document.getElementById("input_image").addEventListener("change", readInputImage);
storeKey = localStorage.getItem("key");
if (storeKey) {
document.getElementById("apiKey").value = storeKey;
} else {
document.getElementById("apiKey").value = null;
}
storesystemPrompt = localStorage.getItem("system_prompt");
if (storesystemPrompt) {
document.getElementById("systemPrompt").value = storesystemPrompt;

View File

@ -1,48 +1,11 @@
/*
https://github.com/david-haerer/chatapi
MIT License
Copyright (c) 2023 David Härer
Copyright (c) 2024 Ettore Di Giacinto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
function submitKey(event) {
event.preventDefault();
localStorage.setItem("key", document.getElementById("apiKey").value);
document.getElementById("apiKey").blur();
}
function genImage(event) {
event.preventDefault();
const input = document.getElementById("input").value;
const key = localStorage.getItem("key");
promptDallE(key, input);
promptDallE(input);
}
async function promptDallE(key, input) {
async function promptDallE(input) {
document.getElementById("loader").style.display = "block";
document.getElementById("input").value = "";
document.getElementById("input").disabled = true;
@ -51,7 +14,6 @@ async function promptDallE(key, input) {
const response = await fetch("v1/images/generations", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
@ -84,13 +46,6 @@ async function promptDallE(key, input) {
document.getElementById("input").focus();
}
document.getElementById("key").addEventListener("submit", submitKey);
document.getElementById("input").focus();
document.getElementById("genimage").addEventListener("submit", genImage);
document.getElementById("loader").style.display = "none";
const storeKey = localStorage.getItem("key");
if (storeKey) {
document.getElementById("apiKey").value = storeKey;
}

View File

@ -9,10 +9,6 @@ let isRecording = false;
let conversationHistory = [];
let resetTimer;
function getApiKey() {
return document.getElementById('apiKey').value;
}
function getModel() {
return document.getElementById('modelSelect').value;
}
@ -99,34 +95,13 @@ function stopRecording() {
};
}
function submitKey(event) {
event.preventDefault();
localStorage.setItem("key", document.getElementById("apiKey").value);
document.getElementById("apiKey").blur();
}
document.getElementById("key").addEventListener("submit", submitKey);
storeKey = localStorage.getItem("key");
if (storeKey) {
document.getElementById("apiKey").value = storeKey;
} else {
document.getElementById("apiKey").value = null;
}
async function sendAudioToWhisper(audioBlob) {
const formData = new FormData();
formData.append('file', audioBlob);
formData.append('model', getWhisperModel());
API_KEY = localStorage.getItem("key");
const response = await fetch('v1/audio/transcriptions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`
},
body: formData
});
@ -137,14 +112,9 @@ async function sendAudioToWhisper(audioBlob) {
async function sendTextToChatGPT(text) {
conversationHistory.push({ role: "user", content: text });
API_KEY = localStorage.getItem("key");
const response = await fetch('v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: getModel(),
messages: conversationHistory
@ -161,13 +131,10 @@ async function sendTextToChatGPT(text) {
}
async function getTextToSpeechAudio(text) {
API_KEY = localStorage.getItem("key");
const response = await fetch('v1/audio/speech', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({

View File

@ -1,52 +1,18 @@
// Initialize Alpine store for API key management
document.addEventListener('alpine:init', () => {
Alpine.store('chat', {
get key() {
return localStorage.getItem('key') || '';
},
set key(value) {
localStorage.setItem('key', value);
}
});
Alpine.store('chat', { });
});
function submitKey(event) {
event.preventDefault();
const keyValue = document.getElementById("apiKey").value;
localStorage.setItem("key", keyValue);
// Show brief visual confirmation
const button = event.submitter;
const originalIcon = button.innerHTML;
button.innerHTML = '<i class="fa-solid fa-check text-green-400"></i>';
button.classList.add('bg-green-600');
button.classList.remove('bg-blue-600', 'hover:bg-blue-700');
setTimeout(() => {
button.innerHTML = originalIcon;
button.classList.remove('bg-green-600');
button.classList.add('bg-blue-600', 'hover:bg-blue-700');
}, 1000);
document.getElementById("apiKey").blur();
}
function genAudio(event) {
event.preventDefault();
const input = document.getElementById("input").value;
const key = localStorage.getItem("key");
if (!input.trim()) {
showNotification('error', 'Please enter text to convert to speech');
return;
}
if (!key) {
showNotification('warning', 'API key is not set. Please set your API key first.');
return;
}
tts(key, input);
tts(input);
}
function showNotification(type, message) {
@ -109,7 +75,7 @@ function showNotification(type, message) {
}, 5000);
}
async function tts(key, input) {
async function tts(input) {
// Show loader and prepare UI
const loader = document.getElementById("loader");
const inputField = document.getElementById("input");
@ -126,7 +92,6 @@ async function tts(key, input) {
const response = await fetch("tts", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
@ -224,17 +189,10 @@ async function tts(key, input) {
// Set up event listeners when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
document.getElementById("key").addEventListener("submit", submitKey);
document.getElementById("input").focus();
document.getElementById("tts").addEventListener("submit", genAudio);
document.getElementById("loader").style.display = "none";
// Initialize stored API key if available
const storeKey = localStorage.getItem("key");
if (storeKey) {
document.getElementById("apiKey").value = storeKey;
}
// Add basic keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Submit on Ctrl+Enter

View File

@ -31,7 +31,7 @@ SOFTWARE.
<script defer src="static/chat.js"></script>
{{ $allGalleryConfigs:=.GalleryConfig }}
{{ $model:=.Model}}
<body class="bg-slate-900 text-gray-100 flex flex-col h-screen" x-data="{ key: $store.chat.key, sidebarOpen: true }">
<body class="bg-slate-900 text-gray-100 flex flex-col h-screen" x-data="{ sidebarOpen: true }">
{{template "views/partials/navbar" .}}
<!-- Main container with sidebar toggle -->
@ -150,36 +150,9 @@ SOFTWARE.
</div>
<!-- Settings tab -->
<div x-show="activeTab === 'settings'" x-data="{ showKeyForm: false, showPromptForm: false }" class="space-y-3">
<div x-show="activeTab === 'settings'" x-data="{ showPromptForm: false }" class="space-y-3">
<button
@click="showKeyForm = !showKeyForm; showPromptForm = false"
class="w-full flex items-center justify-between px-3 py-2 text-sm rounded text-white bg-gray-700 hover:bg-gray-600 transition-colors"
>
<span><i class="fa-solid fa-key mr-2"></i> API Key</span>
<i :class="showKeyForm ? 'fa-chevron-up' : 'fa-chevron-down'" class="fa-solid"></i>
</button>
<div x-show="showKeyForm" class="p-3 bg-gray-700 rounded">
<form id="key" class="flex flex-col space-y-2">
<input
type="password"
id="apiKey"
name="apiKey"
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none"
placeholder="OpenAI API Key"
x-model.lazy="key"
/>
<button
type="submit"
class="px-3 py-2 text-sm rounded text-white bg-blue-600 hover:bg-blue-700 transition-colors"
>
Save API Key
</button>
</form>
</div>
<button
@click="showPromptForm = !showPromptForm; showKeyForm = false"
@click="showPromptForm = !showPromptForm"
class="w-full flex items-center justify-between px-3 py-2 text-sm rounded text-white bg-gray-700 hover:bg-gray-600 transition-colors"
>
<span><i class="fa-solid fa-message mr-2"></i> System Prompt</span>

View File

@ -27,6 +27,8 @@
</div>
</div>
{{template "views/partials/inprogress" .}}
<!-- Search and Filter Section -->
<div class="bg-gray-800/70 rounded-xl p-6 mb-8 shadow-lg border border-gray-700/50">
<!-- Search Input -->

View File

@ -23,42 +23,6 @@
<!-- Talk Interface -->
<div class="max-w-3xl mx-auto">
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden transition-all duration-300 shadow-lg shadow-blue-900/20">
<!-- Key Management Header -->
<div class="border-b border-gray-700 p-4" x-data="{ component: 'menu' }">
<div class="flex items-center justify-center">
<div x-show="component === 'menu'" id="menu">
<button
@click="component = 'key'"
title="Update API key"
class="group flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg"
>
<i class="fas fa-key mr-2"></i>
<span>Set API Key</span>
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
</button>
</div>
<form x-show="component === 'key'" id="key" class="flex items-center space-x-2">
<input
type="password"
id="apiKey"
name="apiKey"
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-lg shadow-sm p-2 appearance-none"
placeholder="API Key"
x-model.lazy="key"
/>
<button
@click="component = 'menu'"
type="submit"
title="Save API key"
class="bg-blue-600 hover:bg-blue-700 text-white p-2 rounded-lg transition duration-300"
>
<i class="fa-solid fa-arrow-right"></i>
</button>
</form>
</div>
</div>
<!-- Talk Interface Body -->
<div class="p-6">
<!-- Recording Status -->

View File

@ -10,7 +10,7 @@
<div class="container mx-auto px-4 py-8 flex-grow" x-data="{ component: 'menu' }">
<!-- Hero Section -->
<div class="bg-gradient-to-r from-blue-900/30 to-indigo-900/30 rounded-2xl shadow-xl p-8 mb-10">
<div class="bg-gradient-to-r from-blue-900/30 to-indigo-900/30 rounded-2xl shadow-xl p-8 mb-6">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
<span class="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-indigo-400">
@ -35,15 +35,16 @@
</div>
</div>
<!-- Model Selection and Controls -->
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl p-6 mb-8 shadow-lg">
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
<!-- Model Selector -->
<div class="w-full md:w-auto">
<label for="model-select" class="block text-sm font-medium text-gray-300 mb-2">Select a Model</label>
<!-- Model Selection - Positioned between hero and generation form -->
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl p-5 mb-6 shadow-lg">
<div class="flex items-center">
<div class="text-lg font-medium text-blue-400 mr-4">
<i class="fas fa-palette mr-2"></i>Select Model:
</div>
<div class="flex-grow">
<select x-data="{ link : '' }" x-model="link" x-init="$watch('link', value => window.location = link)"
id="model-select"
class="bg-gray-900 text-white border border-gray-700 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-lg shadow-sm p-2.5 pr-10 appearance-none w-full md:w-64 transition-colors duration-200"
class="bg-gray-900 text-white border border-gray-700 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-lg shadow-sm p-2.5 pr-10 appearance-none w-full max-w-md transition-colors duration-200"
>
<option value="" disabled class="text-gray-400">Select a model</option>
{{ $model:=.Model}}
@ -60,37 +61,6 @@
{{end}}
</select>
</div>
<!-- API Key Controls -->
<div class="w-full md:w-auto">
<div x-show="component === 'menu'" id="menu" class="flex justify-end">
<button @click="component = 'key'" title="Update API key"
class="flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition duration-300 ease-in-out transform hover:shadow-lg"
>
<i class="fas fa-key mr-2"></i>
<span>Set API Key</span>
</button>
</div>
<form x-show="component === 'key'" id="key" class="flex items-center space-x-2">
<div class="relative">
<input
type="password"
id="apiKey"
name="apiKey"
placeholder="OpenAI API Key"
x-model.lazy="key"
class="pl-3 pr-10 py-2 bg-gray-900 border border-gray-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 rounded-lg text-gray-200"
/>
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
<i class="fas fa-lock text-gray-500"></i>
</div>
</div>
<button @click="component = 'menu'" type="submit" title="Save API key"
class="bg-green-600 hover:bg-green-700 text-white p-2 rounded-lg transition duration-300 ease-in-out">
<i class="fas fa-save"></i>
</button>
</form>
</div>
</div>
</div>
@ -127,7 +97,7 @@
<div class="mt-6 border-t border-gray-700/50 pt-6">
<h3 class="text-xl font-semibold text-gray-200 mb-4">Generated Image</h3>
<div class="container mx-auto flex justify-center">
<div id="result" class="mx-auto bg-gray-900/50 border border-gray-700/50 rounded-xl p-4 min-h-[300px] flex items-center justify-center">
<div id="result" class="mx-auto bg-gray-900/50 border border-gray-700/50 rounded-xl p-4 min-h-[300px] w-full flex items-center justify-center">
<p class="text-gray-400 italic">Your generated image will appear here</p>
</div>
</div>

View File

@ -3,7 +3,7 @@
{{template "views/partials/head" .}}
<script defer src="static/tts.js"></script>
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200" x-data="{ component: 'menu', key: $store.chat.key }">
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
<div class="flex flex-col min-h-screen">
{{template "views/partials/navbar" .}}
@ -31,39 +31,9 @@
<!-- TTS Interface -->
<div class="max-w-3xl mx-auto">
<div class="bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden transition-all duration-300 shadow-lg shadow-blue-900/20">
<!-- Header with API Key and Model Selection -->
<!-- Header with Model Selection -->
<div class="border-b border-gray-700 p-5">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<!-- API Key Management -->
<div>
<div x-show="component === 'menu'" id="menu">
<button @click="component = 'key'" title="Update API key"
class="group flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-105 hover:shadow-lg">
<i class="fas fa-key mr-2"></i>
<span>Set API Key</span>
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
</button>
</div>
<form x-show="component === 'key'" id="key" class="flex items-center space-x-2">
<input
type="password"
id="apiKey"
name="apiKey"
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-lg shadow-sm p-2 appearance-none"
placeholder="OpenAI API Key"
x-model.lazy="key"
/>
<button
@click="component = 'menu'"
type="submit"
title="Save API key"
class="bg-blue-600 hover:bg-blue-700 text-white p-2 rounded-lg transition duration-300">
<i class="fa-solid fa-lock"></i>
</button>
</form>
</div>
<!-- Model Selection -->
<div class="flex items-center">
<label for="model-select" class="mr-3 text-gray-300 font-medium">