Files
hejyou_content_creation/static/script.js
admin 4cd8a63a3d Polygon-Schließen per Button + M2M-Setup-Route entfernt
- Polygon kann nun mit ≥2 Punkten über den Button geschlossen werden
- Button zeigt "Polygon schließen & hinzufügen" solange Polygon offen ist
- Automatisches Schließen (Verbindung zum Startpunkt) beim Klick
- Einmalige Setup-Route /api/directus/setup-m2m entfernt (nicht mehr benötigt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:53:14 +02:00

1136 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 updateAddSelectionBtn() {
if (!addSelectionBtn) return;
if (mode === "polygon") {
const canClose = polygonPoints.length >= 2 && !isPolygonClosed && currentFilename;
const canAdd = isPolygonClosed && polygonPoints.length >= 3 && currentFilename;
addSelectionBtn.disabled = !(canClose || canAdd);
if (canClose && !canAdd) {
addSelectionBtn.textContent = "🔒 Polygon schließen & hinzufügen";
} else {
addSelectionBtn.textContent = " Auswahl hinzufügen";
}
}
}
function resetSelection() {
isDragging = false;
startX = startY = currentX = currentY = 0;
polygonPoints = [];
isPolygonClosed = false;
if (addSelectionBtn) {
addSelectionBtn.disabled = true;
addSelectionBtn.textContent = " Auswahl hinzufügen";
}
drawImageAndSelection();
}
// Funktion zum Rendern der Auswahlen-Liste
function renderSelectionsList() {
if (!selectionsListEl) return;
if (!currentSelections || currentSelections.length === 0) {
selectionsListEl.innerHTML = '<div class="selections-empty">Noch keine Auswahlen hinzugefügt.</div>';
if (saveBtn) {
saveBtn.disabled = true;
}
return;
}
selectionsListEl.innerHTML = currentSelections.map((sel, idx) => {
const num = idx + 1;
if (sel.mode === "rect") {
return `
<div class="selection-item">
<strong>Auswahl ${num}</strong> (Rechteck):
x=${sel.bbox.x}, y=${sel.bbox.y}, w=${sel.bbox.width}, h=${sel.bbox.height}
</div>
`;
} else {
return `
<div class="selection-item">
<strong>Auswahl ${num}</strong> (Polygon):
${sel.polygon ? sel.polygon.length + " Punkte" : ""}
</div>
`;
}
}).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 (polygonPoints.length < 3) {
setStatus("Polygon braucht mindestens 3 Punkte.", true);
return;
}
// Automatisch schließen falls noch nicht geschlossen
if (!isPolygonClosed) {
isPolygonClosed = true;
drawImageAndSelection();
}
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 = '<div class="object-item-text">Noch keine Objekte gespeichert.</div>';
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;
updateAddSelectionBtn();
}
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;
addSelectionBtn.textContent = " Auswahl hinzufügen";
}
} else if (mode === "polygon") {
// Doppelklick = Polygon schließen
if (e.detail === 2 && polygonPoints.length >= 3) {
isPolygonClosed = true;
}
updateAddSelectionBtn();
}
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);
}
});
}
// KIDetails 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 = "⏳ KIDetails...";
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 KIDetails.");
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 = '<div class="sentence-item-empty">Fehler beim Laden der Sätze.</div>';
}
}
}
// Funktion zum Rendern der Sätze-Liste
function renderSentencesList(sentences) {
if (!sentencesListEl) return;
if (!sentences || sentences.length === 0) {
sentencesListEl.innerHTML = '<div class="sentence-item-empty">Noch keine Sätze vorhanden.</div>';
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 `
<div class="sentence-item">
<div style="font-weight: 600; color: #1e40af; margin-bottom: 4px; font-size: 0.85rem;">Einfach:</div>
<div class="sentence-item-question">${escapeHtml(questionSimple)}</div>
<div class="sentence-item-answer">${escapeHtml(answerSimple)}</div>
<div style="font-weight: 600; color: #1e40af; margin-top: 8px; margin-bottom: 4px; font-size: 0.85rem;">Fortgeschritten:</div>
<div class="sentence-item-question">${escapeHtml(questionAdvanced)}</div>
<div class="sentence-item-answer">${escapeHtml(answerAdvanced)}</div>
</div>
`;
}).join("");
}
// Hilfsfunktion zum Escapen von HTML
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// KISentence 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 = "⏳ KISentence...";
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 KISentence.");
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 KISentence.");
} finally {
generateSentenceBtn.disabled = false;
generateSentenceBtn.textContent = originalText;
}
});
}