const prevImageBtn = document.getElementById("prevImageBtn");
const nextImageBtn = document.getElementById("nextImageBtn");
const currentImageNameEl = document.getElementById("currentImageName");
const saveImageNavBtn = document.getElementById("saveImageBtn");
const canvas = document.getElementById("imageCanvas");
const ctx = canvas ? canvas.getContext("2d") : null;
const saveBtn = document.getElementById("saveCropBtn");
const addSelectionBtn = document.getElementById("addSelectionBtn");
const clearAllSelectionsBtn = document.getElementById("clearAllSelectionsBtn");
const selectionsListEl = document.getElementById("selectionsList");
const statusEl = document.getElementById("status");
const modeInputs = document.querySelectorAll('input[name="mode"]');
const clearSelectionBtn = document.getElementById("clearSelectionBtn");
const titleInput = document.getElementById("title_de");
const positionInput = document.getElementById("position_de");
const actionInput = document.getElementById("action_de");
const conditionInput = document.getElementById("condition_de");
const objectsListEl = document.getElementById("objectsList");
const objectsTagsEl = document.getElementById("objectsTags");
const generateDetailsBtn = document.getElementById("generateDetailsBtn");
const generateSentenceBtn = document.getElementById("generateSentenceBtn");
const detailTitleEl = document.getElementById("detailTitle");
const detailPositionEl = document.getElementById("detailPosition");
const detailActionEl = document.getElementById("detailAction");
const detailConditionEl = document.getElementById("detailCondition");
const detailHierarchyEl = document.getElementById("detailHierarchy");
const detailParentEl = document.getElementById("detailParent");
const detailLabelEnEl = document.getElementById("detailLabelEn");
const detailLabelDeEl = document.getElementById("detailLabelDe");
const detailLabelSeEl = document.getElementById("detailLabelSe");
const detailColorEnEl = document.getElementById("detailColorEn");
const detailAdjectiveEnEl = document.getElementById("detailAdjectiveEn");
const detailActionVerbEnEl = document.getElementById("detailActionVerbEn");
const detailPrepositionEnEl = document.getElementById("detailPrepositionEn");
const detailRelativePositionEnEl = document.getElementById("detailRelativePositionEn");
const detailSeasonEnEl = document.getElementById("detailSeasonEn");
const detailSentenceQuestionSimpleEl = document.getElementById("detailSentenceQuestionSimple");
const detailSentenceAnswerSimpleEl = document.getElementById("detailSentenceAnswerSimple");
const detailSentenceQuestionAdvancedEl = document.getElementById("detailSentenceQuestionAdvanced");
const detailSentenceAnswerAdvancedEl = document.getElementById("detailSentenceAnswerAdvanced");
const sentencesListEl = document.getElementById("sentencesList");
let currentImage = null;
let currentFilename = null;
let isDragging = false;
let startX = 0;
let startY = 0;
let currentX = 0;
let currentY = 0;
let displayScale = 1; // Verhältnis: Canvas-Größe zu Originalbild
let mode = "rect"; // "rect" oder "polygon"
let polygonPoints = [];
let isPolygonClosed = false;
let currentObjects = []; // gespeicherte Objekte (Ausschnitte) zum aktuellen Bild
let selectedObjectId = null;
let currentSelections = []; // Gesammelte Auswahlen für das aktuelle Objekt
let imageList = Array.isArray(window.initialImages) ? window.initialImages : [];
let currentImageIndex =
typeof window.initialImageIndex === "number" ? window.initialImageIndex : imageList.length ? imageList.length - 1 : -1;
const isGeneratePage = window.isGeneratePage === true;
function setStatus(text, isError = false) {
if (!statusEl) {
// Auf Seiten ohne Status-Element (z.B. GenerateIt) nur in der Konsole loggen
if (text) {
console[isError ? "error" : "log"](text);
}
return;
}
statusEl.textContent = text;
statusEl.className = isError ? "status error" : "status ok";
}
function updateDetailsPanel(obj) {
if (!detailTitleEl) return; // Nur auf GenerateIt vorhanden
if (!obj) {
detailTitleEl.textContent = "";
detailPositionEl.textContent = "";
detailActionEl.textContent = "";
detailConditionEl.textContent = "";
detailHierarchyEl.textContent = "";
detailParentEl.textContent = "";
if (detailLabelEnEl) {
detailLabelEnEl.textContent = "";
detailLabelDeEl.textContent = "";
detailLabelSeEl.textContent = "";
detailColorEnEl.textContent = "";
detailAdjectiveEnEl.textContent = "";
detailActionVerbEnEl.textContent = "";
detailPrepositionEnEl.textContent = "";
detailRelativePositionEnEl.textContent = "";
detailSeasonEnEl.textContent = "";
}
if (detailSentenceQuestionSimpleEl) detailSentenceQuestionSimpleEl.textContent = "";
if (detailSentenceAnswerSimpleEl) detailSentenceAnswerSimpleEl.textContent = "";
if (detailSentenceQuestionAdvancedEl) detailSentenceQuestionAdvancedEl.textContent = "";
if (detailSentenceAnswerAdvancedEl) detailSentenceAnswerAdvancedEl.textContent = "";
return;
}
// Debug: prüfen, welches Objekt gerade angezeigt werden soll
try {
console.log("updateDetailsPanel für Objekt:", obj.id, obj);
} catch (e) {
console.log("updateDetailsPanel aufgerufen", obj);
}
detailTitleEl.textContent = obj.title_de || "";
detailPositionEl.textContent = obj.position_de || "";
detailActionEl.textContent = obj.action_de || "";
detailConditionEl.textContent = obj.condition_de || "";
detailHierarchyEl.textContent = obj.hierarchy != null ? String(obj.hierarchy) : "";
// Parent-Index und Name: anhand der index-Eigenschaft und title_de des Parent-Objekts bestimmen
let parentDisplay = "";
if (obj.parent_id && Array.isArray(currentObjects)) {
const parent = currentObjects.find((o) => o.id === obj.parent_id);
if (parent && typeof parent.index === "number") {
const parentName = parent.title_de || "ohne Titel";
parentDisplay = `${parent.index} - ${parentName}`;
}
}
detailParentEl.textContent = parentDisplay;
if (detailLabelEnEl) {
detailLabelEnEl.textContent = obj.label_en || "";
if (detailLabelDeEl) detailLabelDeEl.textContent = obj.label_de || "";
if (detailLabelSeEl) detailLabelSeEl.textContent = obj.label_se || "";
if (detailColorEnEl) detailColorEnEl.textContent = obj.color_en || "";
if (detailAdjectiveEnEl) detailAdjectiveEnEl.textContent = obj.adjective_en || "";
if (detailActionVerbEnEl) detailActionVerbEnEl.textContent = obj.action_verb_en || "";
if (detailPrepositionEnEl) detailPrepositionEnEl.textContent = obj.preposition_en || "";
if (detailRelativePositionEnEl) detailRelativePositionEnEl.textContent = obj.relative_position_en || "";
if (detailSeasonEnEl) detailSeasonEnEl.textContent = obj.season_en || "";
}
if (detailSentenceQuestionSimpleEl && obj.latest_sentence) {
detailSentenceQuestionSimpleEl.textContent = obj.latest_sentence.question_simple_en || "";
detailSentenceAnswerSimpleEl.textContent = obj.latest_sentence.answer_simple_en || "";
detailSentenceQuestionAdvancedEl.textContent = obj.latest_sentence.question_advanced_en || "";
detailSentenceAnswerAdvancedEl.textContent = obj.latest_sentence.answer_advanced_en || "";
}
}
function clearCanvas() {
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function drawImageAndSelection() {
if (!currentImage || !ctx || !canvas) return;
clearCanvas();
// Bild vollständig in der aktuellen Canvas-Größe zeichnen (skaliert, ohne Beschnitt)
ctx.drawImage(currentImage, 0, 0, canvas.width, canvas.height);
// Gespeicherte Objekte als farbige Rahmen/Polygone einzeichnen
if (currentObjects && currentObjects.length > 0) {
for (const obj of currentObjects) {
if (!obj.visible) continue;
const bbox = obj.bbox;
const polygon = obj.polygon;
const hierarchy = obj.hierarchy || 1;
const isSelected = selectedObjectId && obj.id === selectedObjectId;
const indexLabel = typeof obj.index === "number" ? String(obj.index) : "";
ctx.save();
// Linien- und Füllfarbe nach Hierarchie
let stroke = "#14532d";
let fill = "rgba(20, 83, 45, 0.2)"; // Fallback
if (hierarchy === 1) {
stroke = "#6b7280"; // grau
fill = "rgba(107, 114, 128, 0.2)"; // 20 %
} else if (hierarchy === 2) {
stroke = "#eab308"; // gelb
fill = "rgba(234, 179, 8, 0.3)"; // 30 %
} else if (hierarchy === 3) {
stroke = "#dc2626"; // rot
fill = "rgba(220, 38, 38, 0.3)"; // 30 %
}
ctx.strokeStyle = stroke;
ctx.fillStyle = fill;
ctx.lineWidth = isSelected ? 3 : 2;
ctx.setLineDash(isSelected ? [2, 2] : [4, 3]);
if (polygon && Array.isArray(polygon) && polygon.length >= 3) {
ctx.beginPath();
ctx.moveTo(polygon[0].x * displayScale, polygon[0].y * displayScale);
for (let i = 1; i < polygon.length; i++) {
ctx.lineTo(polygon[i].x * displayScale, polygon[i].y * displayScale);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
} else if (bbox) {
const bx = bbox.x * displayScale;
const by = bbox.y * displayScale;
const bw = bbox.width * displayScale;
const bh = bbox.height * displayScale;
ctx.fillRect(bx, by, bw, bh);
ctx.strokeRect(bx, by, bw, bh);
}
// Zusätzlicher weißer Rand für hervorgehobenes Objekt
if (isSelected) {
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 2;
ctx.setLineDash([]);
if (polygon && Array.isArray(polygon) && polygon.length >= 3) {
ctx.beginPath();
ctx.moveTo(polygon[0].x * displayScale, polygon[0].y * displayScale);
for (let i = 1; i < polygon.length; i++) {
ctx.lineTo(polygon[i].x * displayScale, polygon[i].y * displayScale);
}
ctx.closePath();
ctx.stroke();
} else if (bbox) {
const bx = bbox.x * displayScale;
const by = bbox.y * displayScale;
const bw = bbox.width * displayScale;
const bh = bbox.height * displayScale;
ctx.strokeRect(bx, by, bw, bh);
}
}
// Index-Zahl in der Mitte des Objekts anzeigen
if (indexLabel) {
let centerX;
let centerY;
if (bbox) {
centerX = (bbox.x + bbox.width / 2) * displayScale;
centerY = (bbox.y + bbox.height / 2) * displayScale;
} else if (polygon && Array.isArray(polygon) && polygon.length > 0) {
const xs = polygon.map((p) => p.x);
const ys = polygon.map((p) => p.y);
centerX = (Math.min(...xs) + Math.max(...xs)) / 2 * displayScale;
centerY = (Math.min(...ys) + Math.max(...ys)) / 2 * displayScale;
}
if (centerX != null && centerY != null) {
ctx.save();
ctx.font = "bold 12px system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// dunkler Hintergrundkreis
ctx.fillStyle = "rgba(15, 23, 42, 0.7)";
ctx.beginPath();
ctx.arc(centerX, centerY, 10, 0, Math.PI * 2);
ctx.fill();
// weiße Zahl
ctx.fillStyle = "#ffffff";
ctx.fillText(indexLabel, centerX, centerY + 0.5);
ctx.restore();
}
}
ctx.restore();
}
}
if (mode === "rect") {
if (isDragging || (startX !== currentX && startY !== currentY)) {
const x = Math.min(startX, currentX);
const y = Math.min(startY, currentY);
const w = Math.abs(currentX - startX);
const h = Math.abs(currentY - startY);
if (w > 0 && h > 0) {
ctx.save();
ctx.strokeStyle = "#f97316"; // Neon-Orange für neue Objekte
ctx.fillStyle = "rgba(249, 115, 22, 0.3)"; // 30 % Füllung
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.fillRect(x, y, w, h);
ctx.strokeRect(x, y, w, h);
ctx.restore();
}
}
} else if (mode === "polygon") {
if (polygonPoints.length > 0) {
ctx.save();
ctx.strokeStyle = "#f97316"; // Neon-Orange für neue Objekte
ctx.fillStyle = "rgba(249, 115, 22, 0.3)"; // 30 % Füllung
ctx.lineWidth = 2;
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
for (let i = 1; i < polygonPoints.length; i++) {
ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
}
if (!isPolygonClosed && isDragging) {
// Vorschau-Linie zur aktuellen Mausposition
ctx.lineTo(currentX, currentY);
}
if (isPolygonClosed) {
ctx.closePath();
ctx.fill();
}
ctx.stroke();
// Punkte markieren
for (const p of polygonPoints) {
ctx.beginPath();
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
ctx.fillStyle = "#ea580c";
ctx.fill();
}
ctx.restore();
}
}
}
function resetSelection() {
isDragging = false;
startX = startY = currentX = currentY = 0;
polygonPoints = [];
isPolygonClosed = false;
if (addSelectionBtn) {
addSelectionBtn.disabled = true;
}
drawImageAndSelection();
}
// Funktion zum Rendern der Auswahlen-Liste
function renderSelectionsList() {
if (!selectionsListEl) return;
if (!currentSelections || currentSelections.length === 0) {
selectionsListEl.innerHTML = '
Noch keine Auswahlen hinzugefügt.
';
if (saveBtn) {
saveBtn.disabled = true;
}
return;
}
selectionsListEl.innerHTML = currentSelections.map((sel, idx) => {
const num = idx + 1;
if (sel.mode === "rect") {
return `
Auswahl ${num} (Rechteck):
x=${sel.bbox.x}, y=${sel.bbox.y}, w=${sel.bbox.width}, h=${sel.bbox.height}
`;
} else {
return `
Auswahl ${num} (Polygon):
${sel.polygon ? sel.polygon.length + " Punkte" : "–"}
`;
}
}).join("");
if (saveBtn) {
saveBtn.disabled = !currentFilename || currentSelections.length === 0;
}
}
// Auswahl zur Liste hinzufügen
function addCurrentSelection() {
if (!currentFilename) return;
let selection = null;
if (mode === "rect") {
const w = Math.abs(currentX - startX);
const h = Math.abs(currentY - startY);
if (w <= 0 || h <= 0) {
setStatus("Bitte zuerst einen Rechteck-Bereich auswählen.", true);
return;
}
const x = Math.min(startX, currentX);
const y = Math.min(startY, currentY);
selection = {
mode: "rect",
bbox: {
x: Math.round(x / displayScale),
y: Math.round(y / displayScale),
width: Math.round(w / displayScale),
height: Math.round(h / displayScale),
},
};
} else if (mode === "polygon") {
if (!isPolygonClosed || polygonPoints.length < 3) {
setStatus("Bitte Polygon mit Doppelklick schließen (mind. 3 Punkte).", true);
return;
}
selection = {
mode: "polygon",
polygon: polygonPoints.map((p) => ({
x: Math.round(p.x / displayScale),
y: Math.round(p.y / displayScale),
})),
};
}
if (selection) {
currentSelections.push(selection);
renderSelectionsList();
resetSelection();
setStatus(`Auswahl ${currentSelections.length} hinzugefügt.`);
}
}
async function loadObjectsForCurrentImage() {
if (!currentFilename) {
currentObjects = [];
if (objectsListEl) objectsListEl.innerHTML = "";
if (objectsTagsEl) objectsTagsEl.innerHTML = "";
selectedObjectId = null;
updateDetailsPanel(null);
return;
}
try {
const res = await fetch(`/api/objects?filename=${encodeURIComponent(currentFilename)}`);
if (!res.ok) {
throw new Error("Fehler beim Laden der Objekte");
}
const data = await res.json();
const objects = (data.objects || []).map((o) => ({
...o,
visible: true,
}));
currentObjects = objects;
if (!selectedObjectId && objects.length > 0) {
selectedObjectId = objects[0].id;
}
if (!objectsListEl) {
// Auf Seiten ohne Liste (falls später nötig)
if (isGeneratePage && objects.length > 0) {
updateDetailsPanel(objects[0]);
}
return;
}
if (objects.length === 0) {
objectsListEl.innerHTML = 'Noch keine Objekte gespeichert.
';
if (objectsTagsEl) objectsTagsEl.innerHTML = "";
updateDetailsPanel(null);
return;
}
objectsListEl.innerHTML = "";
if (objectsTagsEl) objectsTagsEl.innerHTML = "";
for (const obj of objects) {
const wrapper = document.createElement("div");
wrapper.className = "object-item";
const header = document.createElement("div");
header.className = "object-item-header";
if (!isGeneratePage) {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = true;
checkbox.addEventListener("click", (ev) => {
ev.stopPropagation();
const found = currentObjects.find((o) => o.id === obj.id);
if (found) {
found.visible = checkbox.checked;
drawImageAndSelection();
}
});
header.appendChild(checkbox);
}
const img = document.createElement("img");
if (obj.image_file) {
img.src = `/objects_image/${encodeURIComponent(obj.image_file)}`;
img.alt = obj.title_de || obj.id || "";
}
const select = document.createElement("select");
select.className = "object-hierarchy-select";
[1, 2, 3].forEach((level) => {
const opt = document.createElement("option");
opt.value = String(level);
opt.textContent = String(level);
if ((obj.hierarchy || 1) === level) {
opt.selected = true;
}
select.appendChild(opt);
});
select.addEventListener("click", (ev) => ev.stopPropagation());
select.addEventListener("change", async () => {
const newVal = parseInt(select.value, 10);
const found = currentObjects.find((o) => o.id === obj.id);
if (found) {
found.hierarchy = newVal;
}
try {
await fetch(`/api/object/${encodeURIComponent(obj.id)}/hierarchy`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hierarchy: newVal }),
});
} catch (err) {
console.error("Fehler beim Speichern der Hierarchie", err);
}
drawImageAndSelection();
});
const parentSelect = document.createElement("select");
parentSelect.className = "object-parent-select";
const noneOpt = document.createElement("option");
noneOpt.value = "";
noneOpt.textContent = "-";
parentSelect.appendChild(noneOpt);
for (const other of objects) {
if (other.id === obj.id) continue;
const opt = document.createElement("option");
opt.value = other.id;
opt.textContent = String(other.index);
if (obj.parent_id && obj.parent_id === other.id) {
opt.selected = true;
}
parentSelect.appendChild(opt);
}
parentSelect.addEventListener("click", (ev) => ev.stopPropagation());
parentSelect.addEventListener("change", async () => {
const value = parentSelect.value || null;
const found = currentObjects.find((o) => o.id === obj.id);
if (found) {
found.parent_id = value;
}
try {
await fetch(`/api/object/${encodeURIComponent(obj.id)}/parent`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parent_id: value }),
});
} catch (err) {
console.error("Fehler beim Speichern der Parent-Relation", err);
}
});
const text = document.createElement("div");
text.className = "object-item-text";
const title = document.createElement("strong");
title.textContent = obj.title_de || obj.id || "Ohne Titel";
const subtitle = document.createElement("span");
subtitle.textContent = obj.position_de || "";
text.appendChild(title);
if (subtitle.textContent) text.appendChild(subtitle);
header.appendChild(img);
header.appendChild(select);
header.appendChild(parentSelect);
header.appendChild(text);
wrapper.appendChild(header);
if (!isGeneratePage) {
const details = document.createElement("div");
details.className = "object-item-details";
const makeRow = (labelText, key, placeholder = "") => {
const row = document.createElement("div");
const label = document.createElement("label");
label.textContent = labelText;
const input = document.createElement("input");
input.type = "text";
input.value = obj[key] || "";
if (placeholder) input.placeholder = placeholder;
input.addEventListener("click", (ev) => ev.stopPropagation());
input.addEventListener("change", () => {
const found = currentObjects.find((o) => o.id === obj.id);
if (found) found[key] = input.value;
});
row.appendChild(label);
row.appendChild(input);
return { row, input };
};
const { row: titleRow, input: titleInputLocal } = makeRow("Titel", "title_de");
const { row: posRow, input: posInputLocal } = makeRow("Position", "position_de");
const { row: actionRow, input: actionInputLocal } = makeRow("Status", "action_de", "z.B. sitzt");
const { row: condRow, input: condInputLocal } = makeRow("Zustand", "condition_de", "z.B. rostig");
const saveBtn = document.createElement("button");
saveBtn.type = "button";
saveBtn.className = "object-icon-button";
saveBtn.textContent = "💾";
saveBtn.addEventListener("click", async (ev) => {
ev.stopPropagation();
const payload = {
title_de: titleInputLocal.value,
position_de: posInputLocal.value,
action_de: actionInputLocal.value,
condition_de: condInputLocal.value,
};
try {
await fetch(`/api/object/${encodeURIComponent(obj.id)}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const found = currentObjects.find((o) => o.id === obj.id);
if (found) {
Object.assign(found, payload);
}
// Text im Header aktualisieren
title.textContent = payload.title_de || obj.id || "Ohne Titel";
subtitle.textContent = payload.position_de || "";
// Details wieder schließen
details.classList.remove("visible");
} catch (err) {
console.error("Fehler beim Speichern der Objekt-Metadaten", err);
}
});
details.appendChild(titleRow);
details.appendChild(posRow);
details.appendChild(actionRow);
details.appendChild(condRow);
details.appendChild(saveBtn);
wrapper.appendChild(details);
const editBtn = document.createElement("button");
editBtn.type = "button";
editBtn.className = "object-icon-button";
editBtn.textContent = "📝";
editBtn.addEventListener("click", (ev) => {
ev.stopPropagation();
selectedObjectId = obj.id;
drawImageAndSelection();
details.classList.toggle("visible");
});
header.appendChild(editBtn);
}
wrapper.addEventListener("click", () => {
selectedObjectId = obj.id;
drawImageAndSelection();
if (isGeneratePage) {
updateDetailsPanel(obj);
loadSentencesForObject(obj.id);
}
});
objectsListEl.appendChild(wrapper);
}
// Tags mit allen Objektnamen unter der Liste
if (objectsTagsEl) {
for (const obj of objects) {
const tag = document.createElement("span");
tag.className = "object-tag";
tag.textContent = obj.title_de || obj.id || "Ohne Titel";
objectsTagsEl.appendChild(tag);
}
}
// Standard: erstes Objekt im Detail anzeigen (GenerateIt)
if (isGeneratePage && objects.length > 0) {
const first = objects[0];
if (!selectedObjectId) {
selectedObjectId = first.id;
}
updateDetailsPanel(first);
loadSentencesForObject(first.id);
}
} catch (e) {
console.error(e);
}
}
modeInputs.forEach((input) => {
input.addEventListener("change", () => {
mode = input.value;
resetSelection();
});
});
if (clearSelectionBtn) {
clearSelectionBtn.addEventListener("click", () => {
resetSelection();
});
}
function updateImageNav() {
const hasImages = imageList.length > 0 && currentImageIndex >= 0;
if (currentImageNameEl) {
currentImageNameEl.textContent = hasImages ? imageList[currentImageIndex] : "–";
}
if (prevImageBtn) {
prevImageBtn.disabled = !hasImages || currentImageIndex <= 0;
}
if (nextImageBtn) {
nextImageBtn.disabled = !hasImages || currentImageIndex >= imageList.length - 1;
}
if (saveImageNavBtn) {
saveImageNavBtn.disabled = !hasImages;
}
}
function loadCurrentImage() {
if (!imageList.length || currentImageIndex < 0 || currentImageIndex >= imageList.length) {
currentFilename = null;
currentImage = null;
clearCanvas();
updateImageNav();
loadObjectsForCurrentImage();
return;
}
const filename = imageList[currentImageIndex];
currentFilename = filename;
if (saveBtn) {
saveBtn.disabled = true;
}
setStatus("");
clearCanvas();
updateImageNav();
if (!canvas || !ctx) {
// Kein Canvas vorhanden (z.B. GenerateIt) -> nur Objekte laden
currentImage = null;
loadObjectsForCurrentImage();
return;
}
const img = new Image();
img.onload = () => {
currentImage = img;
// Bild so skalieren, dass es in die verfügbare Fläche passt (Breite + Höhe)
const wrapper = canvas.parentElement;
const maxWidth = wrapper.clientWidth - 16;
const maxHeight = window.innerHeight * 0.7;
const scale = Math.min(maxWidth / img.width, maxHeight / img.height, 1);
displayScale = isFinite(scale) && scale > 0 ? scale : 1;
canvas.width = img.width * displayScale;
canvas.height = img.height * displayScale;
resetSelection();
drawImageAndSelection();
loadObjectsForCurrentImage();
};
img.onerror = () => {
setStatus("Fehler beim Laden des Bildes.", true);
};
img.src = `/pictures/${encodeURIComponent(filename)}`;
}
if (prevImageBtn) {
prevImageBtn.addEventListener("click", () => {
if (currentImageIndex > 0) {
currentImageIndex -= 1;
loadCurrentImage();
}
});
}
if (nextImageBtn) {
nextImageBtn.addEventListener("click", () => {
if (currentImageIndex < imageList.length - 1) {
currentImageIndex += 1;
loadCurrentImage();
}
});
}
if (saveImageNavBtn) {
saveImageNavBtn.addEventListener("click", async () => {
if (!currentFilename) return;
try {
setStatus("Bild wird gespeichert ...");
const res = await fetch("/api/image/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: currentFilename }),
});
const data = await res.json();
if (!res.ok) {
setStatus(data.error || "Fehler beim Speichern des Bildes.", true);
return;
}
// Seite neu laden, damit das Bild aus der Übersicht verschwindet
window.location.href = "/draw";
} catch (err) {
console.error(err);
setStatus("Netzwerk-/Serverfehler beim Bild-Speichern.", true);
}
});
}
// Initiales Bild laden (standardmäßig das zuletzt geänderte)
loadCurrentImage();
if (canvas) {
canvas.addEventListener("mousedown", (e) => {
if (!currentImage) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
if (mode === "rect") {
startX = x;
startY = y;
currentX = startX;
currentY = startY;
isDragging = true;
} else if (mode === "polygon") {
if (isPolygonClosed) {
// neue Polygon-Auswahl starten
polygonPoints = [];
isPolygonClosed = false;
}
polygonPoints.push({ x, y });
isDragging = true;
}
drawImageAndSelection();
});
canvas.addEventListener("mousemove", (e) => {
if (!currentImage) return;
if (!isDragging) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
currentX = (e.clientX - rect.left) * scaleX;
currentY = (e.clientY - rect.top) * scaleY;
drawImageAndSelection();
});
canvas.addEventListener("mouseup", (e) => {
if (!currentImage) return;
isDragging = false;
if (mode === "rect") {
const w = Math.abs(currentX - startX);
const h = Math.abs(currentY - startY);
if (addSelectionBtn) {
addSelectionBtn.disabled = w <= 0 || h <= 0 || !currentFilename;
}
} else if (mode === "polygon") {
// Doppelklick = Polygon schließen
if (e.detail === 2 && polygonPoints.length >= 3) {
isPolygonClosed = true;
if (addSelectionBtn) {
addSelectionBtn.disabled = !currentFilename;
}
}
}
drawImageAndSelection();
});
canvas.addEventListener("mouseleave", () => {
if (!currentImage) return;
if (isDragging) {
isDragging = false;
drawImageAndSelection();
}
});
}
// Button: Auswahl hinzufügen
if (addSelectionBtn) {
addSelectionBtn.addEventListener("click", () => {
addCurrentSelection();
});
}
// Button: Alle Auswahlen löschen
if (clearAllSelectionsBtn) {
clearAllSelectionsBtn.addEventListener("click", () => {
currentSelections = [];
renderSelectionsList();
resetSelection();
setStatus("Alle Auswahlen gelöscht.");
});
}
// Button: Objekt speichern (mit allen Auswahlen)
if (saveBtn) {
saveBtn.addEventListener("click", async () => {
if (!currentFilename || !canvas || !ctx) return;
if (!currentSelections || currentSelections.length === 0) {
setStatus("Bitte mindestens eine Auswahl hinzufügen.", true);
return;
}
const payload = {
filename: currentFilename,
selections: currentSelections.map((sel, idx) => ({
number: idx + 1,
mode: sel.mode,
bbox: sel.bbox || null,
polygon: sel.polygon || null,
})),
title_de: titleInput ? titleInput.value || "" : "",
position_de: positionInput ? positionInput.value || "" : "",
action_de: actionInput ? actionInput.value || "" : "",
condition_de: conditionInput ? conditionInput.value || "" : "",
};
try {
setStatus("Speichere Objekt mit allen Auswahlen ...");
const res = await fetch("/api/crop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) {
setStatus(data.error || "Unbekannter Fehler beim Speichern.", true);
return;
}
// Backend liefert: { id, image_file, meta_file }
setStatus(`Gespeichert – ID: ${data.id} (${currentSelections.length} Auswahlen)`);
// Auswahlen zurücksetzen
currentSelections = [];
renderSelectionsList();
resetSelection();
// Metadaten-Felder leeren
if (titleInput) titleInput.value = "";
if (positionInput) positionInput.value = "";
if (actionInput) actionInput.value = "";
if (conditionInput) conditionInput.value = "";
// Liste der Objekte aktualisieren
loadObjectsForCurrentImage();
} catch (err) {
console.error(err);
setStatus("Netzwerk-/Serverfehler beim Speichern.", true);
}
});
}
// KI‑Details für aktuelles Objekt auf GenerateIt erzeugen
if (generateDetailsBtn) {
generateDetailsBtn.addEventListener("click", async () => {
if (!currentObjects.length) {
alert("Keine Objekte für dieses Bild vorhanden.");
return;
}
let target = currentObjects.find((o) => o.id === selectedObjectId);
if (!target) {
target = currentObjects[0];
selectedObjectId = target.id;
}
generateDetailsBtn.disabled = true;
const originalText = generateDetailsBtn.textContent;
generateDetailsBtn.textContent = "⏳ KI‑Details...";
try {
const res = await fetch(`/api/object/${encodeURIComponent(target.id)}/generate_details`, {
method: "POST",
});
const data = await res.json();
if (!res.ok) {
console.error("Fehler von /generate_details:", data);
alert(data.error || "Fehler beim Generieren der KI‑Details.");
return;
}
// Objekt in currentObjects aktualisieren
const found = currentObjects.find((o) => o.id === target.id);
if (found) {
console.log("KI-Details erhalten:", data);
Object.assign(found, data);
console.log("Objekt nach Update:", found);
updateDetailsPanel(found);
} else {
console.error("Objekt nicht in currentObjects gefunden:", target.id);
}
} catch (err) {
console.error("Netzwerkfehler bei generate_details", err);
alert("Netzwerkfehler beim Aufruf der KI.");
} finally {
generateDetailsBtn.disabled = false;
generateDetailsBtn.textContent = originalText;
}
});
}
// Sätze für ein Objekt von der API laden und im Panel anzeigen
async function loadSentencesForObject(objectId) {
if (!isGeneratePage) return;
try {
const res = await fetch(`/api/object/${encodeURIComponent(objectId)}/sentences`);
if (!res.ok) return;
const data = await res.json();
const sentences = Array.isArray(data.sentences) ? data.sentences : [];
// Neuesten Satz in der KI-Sentence-Sektion anzeigen
if (detailSentenceQuestionSimpleEl && detailSentenceAnswerSimpleEl && detailSentenceQuestionAdvancedEl && detailSentenceAnswerAdvancedEl) {
if (!sentences.length) {
detailSentenceQuestionSimpleEl.textContent = "";
detailSentenceAnswerSimpleEl.textContent = "";
detailSentenceQuestionAdvancedEl.textContent = "";
detailSentenceAnswerAdvancedEl.textContent = "";
} else {
const last = sentences[sentences.length - 1];
detailSentenceQuestionSimpleEl.textContent = last.question_simple_en || "";
detailSentenceAnswerSimpleEl.textContent = last.answer_simple_en || "";
detailSentenceQuestionAdvancedEl.textContent = last.question_advanced_en || "";
detailSentenceAnswerAdvancedEl.textContent = last.answer_advanced_en || "";
const found = currentObjects.find((o) => o.id === objectId);
if (found) {
found.latest_sentence = last;
}
}
}
// Alle Sätze in der Liste anzeigen
renderSentencesList(sentences);
} catch (err) {
console.error("Fehler beim Laden der Sätze", err);
if (sentencesListEl) {
sentencesListEl.innerHTML = 'Fehler beim Laden der Sätze.
';
}
}
}
// Funktion zum Rendern der Sätze-Liste
function renderSentencesList(sentences) {
if (!sentencesListEl) return;
if (!sentences || sentences.length === 0) {
sentencesListEl.innerHTML = 'Noch keine Sätze vorhanden.
';
return;
}
// Sätze in umgekehrter Reihenfolge anzeigen (neueste zuerst)
const reversed = [...sentences].reverse();
sentencesListEl.innerHTML = reversed.map((sentence, idx) => {
const questionSimple = sentence.question_simple_en || "";
const answerSimple = sentence.answer_simple_en || "";
const questionAdvanced = sentence.question_advanced_en || "";
const answerAdvanced = sentence.answer_advanced_en || "";
return `
Einfach:
${escapeHtml(questionSimple)}
${escapeHtml(answerSimple)}
Fortgeschritten:
${escapeHtml(questionAdvanced)}
${escapeHtml(answerAdvanced)}
`;
}).join("");
}
// Hilfsfunktion zum Escapen von HTML
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// KI‑Sentence für aktuelles Objekt auf GenerateIt erzeugen
if (generateSentenceBtn) {
generateSentenceBtn.addEventListener("click", async () => {
if (!currentObjects.length) {
alert("Keine Objekte für dieses Bild vorhanden.");
return;
}
let target = currentObjects.find((o) => o.id === selectedObjectId);
if (!target) {
target = currentObjects[0];
selectedObjectId = target.id;
}
generateSentenceBtn.disabled = true;
const originalText = generateSentenceBtn.textContent;
generateSentenceBtn.textContent = "⏳ KI‑Sentence...";
try {
const res = await fetch(`/api/object/${encodeURIComponent(target.id)}/generate_sentence`, {
method: "POST",
});
const data = await res.json();
if (!res.ok) {
console.error("Fehler von /generate_sentence:", data);
alert(data.error || "Fehler beim Generieren der KI‑Sentence.");
return;
}
if (data.sentence) {
if (detailSentenceQuestionSimpleEl) detailSentenceQuestionSimpleEl.textContent = data.sentence.question_simple_en || "";
if (detailSentenceAnswerSimpleEl) detailSentenceAnswerSimpleEl.textContent = data.sentence.answer_simple_en || "";
if (detailSentenceQuestionAdvancedEl) detailSentenceQuestionAdvancedEl.textContent = data.sentence.question_advanced_en || "";
if (detailSentenceAnswerAdvancedEl) detailSentenceAnswerAdvancedEl.textContent = data.sentence.answer_advanced_en || "";
const found = currentObjects.find((o) => o.id === target.id);
if (found) {
found.latest_sentence = data.sentence;
}
// Sätze neu laden, um die Liste zu aktualisieren
await loadSentencesForObject(target.id);
}
} catch (err) {
console.error("Netzwerkfehler bei generate_sentence", err);
alert("Netzwerkfehler beim Aufruf der KI‑Sentence.");
} finally {
generateSentenceBtn.disabled = false;
generateSentenceBtn.textContent = originalText;
}
});
}