Add OpenAI chat translation layer
This commit is contained in:
@@ -706,6 +706,20 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
||||
.message .by { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
||||
.message p { margin: 0; white-space: pre-wrap; }
|
||||
.message a { color: var(--brand); display: inline-block; margin-top: 6px; }
|
||||
.translation {
|
||||
margin-top: 7px;
|
||||
padding: 8px 9px;
|
||||
border-left: 3px solid var(--brand);
|
||||
border-radius: 6px;
|
||||
background: #f4faf7;
|
||||
}
|
||||
.translation span {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.read-receipt {
|
||||
align-self: flex-end;
|
||||
margin-top: -4px;
|
||||
|
||||
@@ -165,6 +165,20 @@
|
||||
</label>
|
||||
<label class="setting-switch">Opakovat do otevreni chatu <input id="adminSoundRepeat" type="checkbox"></label>
|
||||
<button id="adminSoundTest" type="button">Otestovat zvuk</button>
|
||||
<fieldset class="effect-settings">
|
||||
<legend>AI preklady</legend>
|
||||
<label><input id="translationEnabled" type="checkbox"> zapnout preklady</label>
|
||||
<label>Jazyk operatora
|
||||
<select id="operatorLanguage">
|
||||
<option value="cs">Cestina</option>
|
||||
<option value="sk">Slovenstina</option>
|
||||
<option value="pl">Polstina</option>
|
||||
<option value="en">Anglictina</option>
|
||||
<option value="de">Nemcina</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>OpenAI model <input id="translationModel" placeholder="gpt-5.6-luna"></label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
+34
-1
@@ -79,6 +79,9 @@ $("#adminSoundRepeat").addEventListener("change", (event) => {
|
||||
$("#adminSoundTest").addEventListener("click", () => {
|
||||
playNotifySound({ ignoreMuted: true });
|
||||
});
|
||||
$("#translationEnabled").addEventListener("change", saveTranslationSettings);
|
||||
$("#operatorLanguage").addEventListener("change", saveTranslationSettings);
|
||||
$("#translationModel").addEventListener("change", saveTranslationSettings);
|
||||
document.querySelectorAll("[data-settings-tab]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const tab = button.dataset.settingsTab;
|
||||
@@ -385,6 +388,21 @@ function renderSite() {
|
||||
form.messageBadge.checked = s.notifications?.badgeOnAdminReply !== false;
|
||||
form.messageLabel.checked = s.notifications?.labelOnAdminReply !== false;
|
||||
form.messageWiggle.checked = s.notifications?.wiggleOnAdminReply !== false;
|
||||
$("#translationEnabled").checked = Boolean(s.translations?.enabled);
|
||||
$("#operatorLanguage").value = s.translations?.operatorLanguage || "cs";
|
||||
$("#translationModel").value = s.translations?.model || "gpt-5.6-luna";
|
||||
}
|
||||
|
||||
async function saveTranslationSettings() {
|
||||
if (!state.site) return;
|
||||
const settings = structuredClone(state.site.settings);
|
||||
settings.translations = {
|
||||
...(settings.translations || {}),
|
||||
enabled: $("#translationEnabled").checked,
|
||||
operatorLanguage: $("#operatorLanguage").value,
|
||||
model: $("#translationModel").value.trim() || "gpt-5.6-luna"
|
||||
};
|
||||
await saveSite({ settings, isOnline: state.site.isOnline });
|
||||
}
|
||||
|
||||
function renderConversations() {
|
||||
@@ -511,13 +529,28 @@ function renderActive() {
|
||||
$("#messages").innerHTML = messages.map((message) => `
|
||||
<article class="message ${message.senderType}">
|
||||
<div class="by">${escapeHtml(message.senderName || message.senderType)} · ${escapeHtml(message.createdAt)}</div>
|
||||
${message.body ? `<p>${escapeHtml(message.body)}</p>` : ""}
|
||||
${renderMessageBody(message)}
|
||||
${renderAttachments(message.attachments)}
|
||||
</article>
|
||||
`).join("") + adminReadReceipt(conversation, messages);
|
||||
$("#messages").scrollTop = $("#messages").scrollHeight;
|
||||
}
|
||||
|
||||
function renderMessageBody(message) {
|
||||
const original = message.body ? `<p>${escapeHtml(message.body)}</p>` : "";
|
||||
if (!message.translatedBody) return original;
|
||||
const label = message.senderType === "visitor"
|
||||
? `Preklad do ${escapeHtml(message.translatedLanguage || "")}`
|
||||
: `Odeslano zakaznikovi ${escapeHtml(message.translatedLanguage || "")}`;
|
||||
return `
|
||||
${original}
|
||||
<div class="translation">
|
||||
<span>${label}</span>
|
||||
<p>${escapeHtml(message.translatedBody)}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function renameVisitor(visitorToken) {
|
||||
if (!state.active?.conversation || !visitorToken) return;
|
||||
const currentName = conversationVisitorName(state.active.conversation);
|
||||
|
||||
+2
-1
@@ -356,7 +356,8 @@
|
||||
messages.innerHTML = "";
|
||||
let lastAdminName = null;
|
||||
for (const message of data.messages) {
|
||||
addMessage(messages, message.senderType, message.senderName, message.body, message.attachments || [], lastAdminName);
|
||||
const body = message.senderType === "admin" ? (message.translatedBody || message.body) : message.body;
|
||||
addMessage(messages, message.senderType, message.senderName, body, message.attachments || [], lastAdminName);
|
||||
if (message.senderType === "admin") lastAdminName = message.senderName || "Operator";
|
||||
else lastAdminName = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user