- 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>
1136 lines
39 KiB
JavaScript
1136 lines
39 KiB
JavaScript
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);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 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 = '<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;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
});
|
||
}
|