Show translation diagnostics in admin
This commit is contained in:
@@ -753,6 +753,27 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
}
|
}
|
||||||
.message.admin { align-self: flex-end; background: #ecf8f3; border-color: #c7e8dc; }
|
.message.admin { align-self: flex-end; background: #ecf8f3; border-color: #c7e8dc; }
|
||||||
.message .by { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
.message .by { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
||||||
|
.translation-meta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #d8e5df;
|
||||||
|
background: #f6faf8;
|
||||||
|
color: #416256;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.translation-meta.translated { border-color: #b9dfca; background: #eef8f2; color: #1e7a47; }
|
||||||
|
.translation-meta.failed { border-color: #f0b9b9; background: #fff1f1; color: #b42318; }
|
||||||
|
.translation-meta.missing_key,
|
||||||
|
.translation-meta.empty { border-color: #f1d29b; background: #fff7e8; color: #9a5b00; }
|
||||||
|
.translation-meta.disabled,
|
||||||
|
.translation-meta.skipped,
|
||||||
|
.translation-meta.pending,
|
||||||
|
.translation-meta.same_language { border-color: #d7dde2; background: #f5f7f8; color: #667085; }
|
||||||
.message p { margin: 0; white-space: pre-wrap; }
|
.message p { margin: 0; white-space: pre-wrap; }
|
||||||
.message a { color: var(--brand); display: inline-block; margin-top: 6px; }
|
.message a { color: var(--brand); display: inline-block; margin-top: 6px; }
|
||||||
.translation {
|
.translation {
|
||||||
|
|||||||
@@ -591,9 +591,47 @@ function renderActive() {
|
|||||||
${renderAttachments(message.attachments)}
|
${renderAttachments(message.attachments)}
|
||||||
</article>
|
</article>
|
||||||
`).join("") + adminReadReceipt(conversation, messages);
|
`).join("") + adminReadReceipt(conversation, messages);
|
||||||
|
decorateTranslationMeta(messages);
|
||||||
$("#messages").scrollTop = $("#messages").scrollHeight;
|
$("#messages").scrollTop = $("#messages").scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decorateTranslationMeta(messages) {
|
||||||
|
document.querySelectorAll("#messages .message .by").forEach((element, index) => {
|
||||||
|
const meta = translationMeta(messages[index]);
|
||||||
|
if (!meta) return;
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = `translation-meta ${meta.status}`;
|
||||||
|
badge.textContent = meta.label;
|
||||||
|
if (meta.error) badge.title = meta.error;
|
||||||
|
element.append(" ");
|
||||||
|
element.append(badge);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function translationMeta(message) {
|
||||||
|
if (!message?.translationStatus) return null;
|
||||||
|
const status = String(message.translationStatus || "");
|
||||||
|
const source = String(message.originalLanguage || "?").toUpperCase();
|
||||||
|
const target = String(message.translatedLanguage || "?").toUpperCase();
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
error: message.translationError || "",
|
||||||
|
label: translationStatusLabel(status, source, target)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function translationStatusLabel(status, source, target) {
|
||||||
|
if (status === "translated") return `preklad: ${source} -> ${target}`;
|
||||||
|
if (status === "same_language") return `preklad: ${source} = ${target}`;
|
||||||
|
if (status === "failed") return `preklad: ${source} -> ${target} - chyba`;
|
||||||
|
if (status === "missing_key") return "preklad: chybi klic";
|
||||||
|
if (status === "disabled") return "preklad: vypnuto";
|
||||||
|
if (status === "empty") return `preklad: ${source} -> ${target} - prazdny`;
|
||||||
|
if (status === "skipped") return "preklad: preskoceno";
|
||||||
|
if (status === "pending") return "preklad: ceka";
|
||||||
|
return `preklad: ${status}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMessageBody(message) {
|
function renderMessageBody(message) {
|
||||||
const original = message.body ? `<p>${escapeHtml(message.body)}</p>` : "";
|
const original = message.body ? `<p>${escapeHtml(message.body)}</p>` : "";
|
||||||
if (!message.translatedBody) return original;
|
if (!message.translatedBody) return original;
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ function initDb() {
|
|||||||
translated_body TEXT,
|
translated_body TEXT,
|
||||||
translated_language TEXT,
|
translated_language TEXT,
|
||||||
translation_status TEXT,
|
translation_status TEXT,
|
||||||
|
translation_error TEXT,
|
||||||
body TEXT NOT NULL,
|
body TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -227,6 +228,7 @@ function initDb() {
|
|||||||
ensureColumn("messages", "translated_body", "TEXT");
|
ensureColumn("messages", "translated_body", "TEXT");
|
||||||
ensureColumn("messages", "translated_language", "TEXT");
|
ensureColumn("messages", "translated_language", "TEXT");
|
||||||
ensureColumn("messages", "translation_status", "TEXT");
|
ensureColumn("messages", "translation_status", "TEXT");
|
||||||
|
ensureColumn("messages", "translation_error", "TEXT");
|
||||||
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_client_id ON messages(conversation_id, client_message_id) WHERE client_message_id IS NOT NULL");
|
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_client_id ON messages(conversation_id, client_message_id) WHERE client_message_id IS NOT NULL");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,17 +778,17 @@ function normalizeClientMessageId(value) {
|
|||||||
|
|
||||||
async function translateStoredMessage(conversationId, messageId, senderType, body) {
|
async function translateStoredMessage(conversationId, messageId, senderType, body) {
|
||||||
const message = db.prepare("SELECT * FROM messages WHERE id = ? AND conversation_id = ?").get(messageId, conversationId);
|
const message = db.prepare("SELECT * FROM messages WHERE id = ? AND conversation_id = ?").get(messageId, conversationId);
|
||||||
if (!message || !message.body.trim()) return markMessageTranslation(messageId, null, null, null, "skipped");
|
if (!message || !message.body.trim()) return markMessageTranslation(messageId, null, null, null, "skipped", null);
|
||||||
|
|
||||||
const conversation = findConversationById(conversationId);
|
const conversation = findConversationById(conversationId);
|
||||||
const settings = getTranslationSettings();
|
const settings = getTranslationSettings();
|
||||||
if (!settings.enabled) return markMessageTranslation(messageId, null, null, null, "disabled");
|
if (!settings.enabled) return markMessageTranslation(messageId, null, null, null, "disabled", null);
|
||||||
if (!settings.openaiApiKey) return markMessageTranslation(messageId, null, null, null, "missing_key");
|
if (!settings.openaiApiKey) return markMessageTranslation(messageId, null, null, null, "missing_key", null);
|
||||||
|
|
||||||
const targetLanguage = senderType === "visitor"
|
const targetLanguage = senderType === "visitor"
|
||||||
? settings.operatorLanguage
|
? settings.operatorLanguage
|
||||||
: normalizeLanguage(conversation.customer_language || conversation.language || body.language || settings.operatorLanguage);
|
: normalizeLanguage(conversation.customer_language || conversation.language || body.language || settings.operatorLanguage);
|
||||||
if (!targetLanguage) return markMessageTranslation(messageId, null, null, null, "skipped");
|
if (!targetLanguage) return markMessageTranslation(messageId, null, null, null, "skipped", null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await translateText(message.body, targetLanguage, settings.model, settings.openaiApiKey);
|
const result = await translateText(message.body, targetLanguage, settings.model, settings.openaiApiKey);
|
||||||
@@ -800,24 +802,25 @@ async function translateStoredMessage(conversationId, messageId, senderType, bod
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!translatedText || sameLanguage) {
|
if (!translatedText || sameLanguage) {
|
||||||
return markMessageTranslation(messageId, sourceLanguage, null, translatedLanguage, sameLanguage ? "same_language" : "empty");
|
return markMessageTranslation(messageId, sourceLanguage, null, translatedLanguage, sameLanguage ? "same_language" : "empty", null);
|
||||||
}
|
}
|
||||||
markMessageTranslation(messageId, sourceLanguage, translatedText, translatedLanguage, "translated");
|
markMessageTranslation(messageId, sourceLanguage, translatedText, translatedLanguage, "translated", null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("translation_failed", error.message);
|
console.error("translation_failed", error.message);
|
||||||
markMessageTranslation(messageId, null, null, targetLanguage, "failed");
|
markMessageTranslation(messageId, null, null, targetLanguage, "failed", error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function markMessageTranslation(messageId, sourceLanguage, translatedBody, translatedLanguage, status) {
|
function markMessageTranslation(messageId, sourceLanguage, translatedBody, translatedLanguage, status, error) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET original_language = ?,
|
SET original_language = ?,
|
||||||
translated_body = ?,
|
translated_body = ?,
|
||||||
translated_language = ?,
|
translated_language = ?,
|
||||||
translation_status = ?
|
translation_status = ?,
|
||||||
|
translation_error = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(sourceLanguage, translatedBody, translatedLanguage, status, messageId);
|
`).run(sourceLanguage, translatedBody, translatedLanguage, status, truncateText(error, 500), messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function translateText(text, targetLanguage, model, apiKey) {
|
async function translateText(text, targetLanguage, model, apiKey) {
|
||||||
@@ -948,6 +951,13 @@ function normalizeLanguage(value) {
|
|||||||
return text.split("-")[0].slice(0, 8);
|
return text.split("-")[0].slice(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function truncateText(value, maxLength) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
if (!text) return null;
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return `${text.slice(0, maxLength - 3)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
function saveAttachments(conversationId, messageId, attachments) {
|
function saveAttachments(conversationId, messageId, attachments) {
|
||||||
for (const attachment of attachments.slice(0, 3)) {
|
for (const attachment of attachments.slice(0, 3)) {
|
||||||
if (!attachment.dataBase64 || !attachment.name) continue;
|
if (!attachment.dataBase64 || !attachment.name) continue;
|
||||||
@@ -1072,6 +1082,7 @@ function conversationDetails(conversation) {
|
|||||||
translatedBody: message.translated_body,
|
translatedBody: message.translated_body,
|
||||||
translatedLanguage: message.translated_language,
|
translatedLanguage: message.translated_language,
|
||||||
translationStatus: message.translation_status,
|
translationStatus: message.translation_status,
|
||||||
|
translationError: message.translation_error,
|
||||||
createdAt: message.created_at,
|
createdAt: message.created_at,
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.filter((item) => item.message_id === message.id)
|
.filter((item) => item.message_id === message.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user