File: //proc/self/root/tmp/voice2image_check_frontend/app.js
const dom = {
recordBtn: document.getElementById("recordButton"),
recordLabel: document.getElementById("recordLabel"),
recIndicator: document.getElementById("recIndicator"),
statusChip: document.getElementById("statusChip"),
stepsInput: document.getElementById("stepsInput"),
stepsValue: document.getElementById("stepsValue"),
cfgInput: document.getElementById("cfgInput"),
cfgValue: document.getElementById("cfgValue"),
seedInput: document.getElementById("seedInput"),
transcriptBox: document.getElementById("transcriptBox"),
promptBox: document.getElementById("promptBox"),
latestImage: document.getElementById("latestImage"),
imageMeta: document.getElementById("imageMeta"),
galleryGrid: document.getElementById("galleryGrid"),
historyList: document.getElementById("historyList"),
historyCount: document.getElementById("historyCount"),
refreshHistory: document.getElementById("refreshHistory"),
settingsHelpButton: document.getElementById("settingsHelpButton"),
settingsHelpModal: document.getElementById("settingsHelpModal"),
settingsHelpClose: document.getElementById("settingsHelpClose"),
};
let mediaRecorder = null;
let chunks = [];
let stream = null;
let isRecording = false;
let historyItems = [];
const setStatus = (text, tone = "idle") => {
dom.statusChip.textContent = text;
dom.recIndicator.textContent = tone === "recording" ? "Recording" : tone === "busy" ? "Busy" : "Idle";
};
const updateSliderReadouts = () => {
dom.stepsValue.textContent = dom.stepsInput.value;
dom.cfgValue.textContent = dom.cfgInput.value;
};
dom.stepsInput.addEventListener("input", updateSliderReadouts);
dom.cfgInput.addEventListener("input", updateSliderReadouts);
updateSliderReadouts();
const supportsMedia = () => "MediaRecorder" in window && navigator.mediaDevices?.getUserMedia;
const startRecording = async () => {
if (!supportsMedia()) {
alert("Dein Browser unterstützt kein MediaRecorder API.");
return;
}
try {
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
chunks = [];
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: "audio/webm" });
chunks = [];
stream.getTracks().forEach((t) => t.stop());
submitClip(blob);
};
mediaRecorder.start();
isRecording = true;
dom.recordLabel.textContent = "Aufnahme stoppen";
dom.recordBtn.classList.add("active");
setStatus("Aufnahme läuft", "recording");
} catch (err) {
console.error(err);
alert("Konnte Mikrofon nicht starten: " + err.message);
}
};
const stopRecording = () => {
if (!mediaRecorder || !isRecording) return;
isRecording = false;
dom.recordLabel.textContent = "Aufnahme starten";
dom.recordBtn.classList.remove("active");
setStatus("Sende an Backend...", "busy");
mediaRecorder.stop();
};
dom.recordBtn.addEventListener("click", () => {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
});
dom.refreshHistory.addEventListener("click", () => {
loadHistory();
});
const openSettingsHelp = () => {
dom.settingsHelpModal.hidden = false;
dom.settingsHelpClose.focus();
};
const closeSettingsHelp = () => {
dom.settingsHelpModal.hidden = true;
dom.settingsHelpButton.focus();
};
dom.settingsHelpButton.addEventListener("click", openSettingsHelp);
dom.settingsHelpClose.addEventListener("click", closeSettingsHelp);
dom.settingsHelpModal.addEventListener("click", (event) => {
if (event.target === dom.settingsHelpModal) closeSettingsHelp();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !dom.settingsHelpModal.hidden) {
closeSettingsHelp();
}
});
const submitClip = async (blob) => {
setStatus("Verarbeite Audio...", "busy");
const form = new FormData();
form.append("audio", blob, "voice.webm");
const seedVal = dom.seedInput.value.trim();
const params = new URLSearchParams({
steps: dom.stepsInput.value,
cfg: dom.cfgInput.value,
});
if (seedVal) params.set("seed", seedVal);
try {
const res = await fetch(`/api/generate?${params.toString()}`, {
method: "POST",
body: form,
});
if (!res.ok) {
const error = await res.json().catch(() => ({}));
throw new Error(error.detail || "Fehler beim Generieren.");
}
const data = await res.json();
prependHistory(data);
renderLatest(data);
setStatus("Fertig", "idle");
} catch (err) {
console.error(err);
setStatus("Fehler: " + err.message, "idle");
alert(err.message);
}
};
const prependHistory = (item) => {
historyItems = [item, ...historyItems];
renderHistory();
renderGallery();
};
const renderLatest = (item) => {
if (!item) {
clearLatest();
return;
}
dom.transcriptBox.textContent = item.transcript || "Keine Transkription.";
dom.promptBox.textContent = item.prompt || "Kein Prompt.";
dom.latestImage.src = item.image_url;
dom.latestImage.dataset.imageId = item.id;
dom.imageMeta.textContent = `Steps ${item.steps} • CFG ${item.cfg} • Seed ${item.seed ?? "random"} • ${item.created_at}`;
};
const clearLatest = () => {
dom.transcriptBox.textContent = "Noch nichts aufgenommen.";
dom.promptBox.textContent = "Warte auf Ergebnis.";
dom.latestImage.removeAttribute("src");
delete dom.latestImage.dataset.imageId;
dom.imageMeta.textContent = "Noch kein Bild generiert.";
};
const renderGallery = () => {
dom.galleryGrid.innerHTML = "";
historyItems.forEach((item) => {
const fig = document.createElement("figure");
const img = document.createElement("img");
img.src = item.image_url;
img.alt = item.prompt.slice(0, 120);
const caption = document.createElement("figcaption");
caption.textContent = item.prompt.slice(0, 120);
const deleteButton = document.createElement("button");
deleteButton.className = "gallery-delete";
deleteButton.type = "button";
deleteButton.textContent = "×";
deleteButton.title = "Bild löschen";
deleteButton.setAttribute("aria-label", "Bild löschen");
deleteButton.dataset.deleteImageId = item.id;
fig.appendChild(img);
fig.appendChild(deleteButton);
fig.appendChild(caption);
dom.galleryGrid.appendChild(fig);
});
dom.historyCount.textContent = `${historyItems.length} Items`;
};
const renderHistory = () => {
dom.historyList.innerHTML = "";
historyItems.forEach((item) => {
const wrap = document.createElement("div");
wrap.className = "item";
const meta = document.createElement("div");
meta.className = "meta";
meta.textContent = `${item.created_at} — Steps ${item.steps} • CFG ${item.cfg} • Seed ${item.seed ?? "random"}`;
const p = document.createElement("p");
p.className = "prompt";
p.textContent = item.prompt;
wrap.appendChild(meta);
wrap.appendChild(p);
dom.historyList.appendChild(wrap);
});
};
const loadHistory = async () => {
try {
const res = await fetch("/api/history");
if (!res.ok) throw new Error("History konnte nicht geladen werden.");
const data = await res.json();
historyItems = data.items || [];
renderHistory();
renderGallery();
if (historyItems.length) renderLatest(historyItems[0]);
setStatus("History geladen", "idle");
} catch (err) {
console.error(err);
setStatus("Konnte History nicht laden", "idle");
}
};
window.voice2imageGallery = {
findItem(imageId) {
return historyItems.find((item) => item.id === imageId);
},
removeItem(imageId) {
const wasLatest = dom.latestImage.dataset.imageId === imageId;
historyItems = historyItems.filter((item) => item.id !== imageId);
renderHistory();
renderGallery();
if (wasLatest) renderLatest(historyItems[0]);
},
setStatus,
};
loadHistory();