Compare commits
10
Commits
0f3c608d86
...
e2b4fec2a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2b4fec2a3 | ||
|
|
17b4665e23 | ||
|
|
bf2c58f90a | ||
|
|
f491b393d7 | ||
|
|
81375b3dd3 | ||
|
|
608d39a4f2 | ||
|
|
332e6d20f4 | ||
|
|
84d8a21d38 | ||
|
|
e9c363639f | ||
|
|
1a6eaa69fe |
@@ -1,5 +1,13 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
*.sql
|
||||||
|
*.zip
|
||||||
|
config/local.json
|
||||||
data/*.sqlite
|
data/*.sqlite
|
||||||
data/*.sqlite-*
|
data/*.sqlite-*
|
||||||
data/uploads/
|
data/uploads/
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# MaalFlows Agent
|
||||||
|
|
||||||
|
Purpose: maintain the MaalFlows live chat widget and admin app.
|
||||||
|
|
||||||
|
## Project Shape
|
||||||
|
|
||||||
|
- Main application file: `server.js`.
|
||||||
|
- Frontend assets: `public/widget.js`, `public/admin.js`, `public/admin.css`, `public/admin.html`.
|
||||||
|
- Runtime: Node.js without a framework, SQLite through `better-sqlite3`.
|
||||||
|
- Local check command: `npm.cmd run check`.
|
||||||
|
- Production app is proxied by nginx on `app.black-week.cz` to `127.0.0.1:3400`.
|
||||||
|
|
||||||
|
## Work Rules
|
||||||
|
|
||||||
|
- Read the relevant code before editing.
|
||||||
|
- Keep changes small and focused on the user's request.
|
||||||
|
- Do not touch `.agents/sogo-mail-filter-agent.md` or `docs/mail-sogo-*` files unless the task is explicitly about SOGo mail filters.
|
||||||
|
- Preserve existing user changes. Do not reset, checkout, or delete unrelated work.
|
||||||
|
- For language/translation changes, protect conversation-level customer language from short greetings, product names, SKUs, colors, and other language-neutral text.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Before finishing a code change, run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm.cmd run check
|
||||||
|
```
|
||||||
|
|
||||||
|
If a broader behavioral change is made, add or run the smallest practical extra verification for that area.
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
After each completed project code change:
|
||||||
|
|
||||||
|
1. Review `git diff`.
|
||||||
|
2. Stage only files that belong to the change.
|
||||||
|
3. Commit with a concise message.
|
||||||
|
4. Push the current branch to `origin`.
|
||||||
|
5. Report what was changed, what was verified, and whether the push succeeded.
|
||||||
|
|
||||||
|
Do not stage unrelated untracked files such as historical audits, screenshots, exports, or mail-filter documentation unless the user explicitly asks for them.
|
||||||
|
|
||||||
|
## Production Update
|
||||||
|
|
||||||
|
After a successful push, update the production server only when the deployment target and restart command are known for this project.
|
||||||
|
|
||||||
|
Expected deployment shape:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh <production-host> "cd <app-directory> && git pull --ff-only && npm install --omit=dev && <restart-command>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Before running production deploy commands, confirm the real host, app directory, and restart command if they are not already documented in the repository or current task context.
|
||||||
@@ -231,6 +231,255 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
width: auto;
|
width: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.launcher-preview-stage {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-height: 112px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgb(15 143 111 / 5%) 1px, transparent 1px),
|
||||||
|
linear-gradient(rgb(15 143 111 / 5%) 1px, transparent 1px),
|
||||||
|
#ffffff;
|
||||||
|
background-size: 18px 18px;
|
||||||
|
}
|
||||||
|
.launcher-preview {
|
||||||
|
--preview-brand: var(--brand);
|
||||||
|
--preview-text: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--preview-brand);
|
||||||
|
color: var(--preview-text);
|
||||||
|
box-shadow: 0 12px 30px rgb(0 0 0 / 22%);
|
||||||
|
cursor: default;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.launcher-preview.effect-breathe {
|
||||||
|
animation: launcherPreviewBreathe 2.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.launcher-preview.effect-ring::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: -5px;
|
||||||
|
border: 2px solid var(--preview-brand);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: .62;
|
||||||
|
content: "";
|
||||||
|
animation: launcherPreviewRing 2.2s ease-out infinite;
|
||||||
|
}
|
||||||
|
.launcher-preview.notify-pulse::after {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
animation: launcherPreviewPulse 1.15s ease-out infinite;
|
||||||
|
}
|
||||||
|
.launcher-preview.notify-wiggle .launcher-preview-icon {
|
||||||
|
animation: launcherPreviewWiggle 7.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.launcher-preview.effect-sway .launcher-preview-icon {
|
||||||
|
animation: launcherPreviewWiggle 7.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.launcher-preview-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 3px solid #ffffff;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #c9d4ce;
|
||||||
|
box-shadow: 0 2px 6px rgb(0 0 0 / 14%);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.launcher-preview.show-status .launcher-preview-status {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.launcher-preview.online .launcher-preview-status {
|
||||||
|
background: #2f9e44;
|
||||||
|
}
|
||||||
|
.launcher-preview-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: none;
|
||||||
|
place-items: center;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ff3b30;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 0 8px 18px rgb(0 0 0 / 18%);
|
||||||
|
}
|
||||||
|
.launcher-preview.show-badge .launcher-preview-badge {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.launcher-preview-label {
|
||||||
|
position: absolute;
|
||||||
|
right: 68px;
|
||||||
|
top: 50%;
|
||||||
|
max-width: 150px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--ink);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 10px 24px rgb(0 0 0 / 18%);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50%) translateX(8px);
|
||||||
|
transition: opacity .18s ease, transform .18s ease;
|
||||||
|
}
|
||||||
|
.launcher-preview.show-label .launcher-preview-label {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-50%) translateX(0);
|
||||||
|
}
|
||||||
|
.launcher-preview.label-always .launcher-preview-label {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-50%) translateX(0);
|
||||||
|
}
|
||||||
|
.launcher-preview-icon,
|
||||||
|
.launcher-preview-icon::before,
|
||||||
|
.launcher-preview-icon::after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
.launcher-preview-icon {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--preview-text);
|
||||||
|
}
|
||||||
|
.preview-shape-circle .launcher-preview-icon,
|
||||||
|
.preview-shape-circle .launcher-preview-icon::before,
|
||||||
|
.preview-shape-circle .launcher-preview-icon::after {
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--preview-text);
|
||||||
|
}
|
||||||
|
.preview-shape-circle .launcher-preview-icon {
|
||||||
|
width: 26px;
|
||||||
|
}
|
||||||
|
.preview-shape-circle .launcher-preview-icon::before {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
.preview-shape-circle .launcher-preview-icon::after {
|
||||||
|
width: 18px;
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
.preview-shape-bubble .launcher-preview-icon {
|
||||||
|
width: 30px;
|
||||||
|
height: 23px;
|
||||||
|
border: 3px solid var(--preview-text);
|
||||||
|
border-radius: 13px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.preview-shape-bubble .launcher-preview-icon::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 7px;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--preview-text);
|
||||||
|
box-shadow: 8px 0 0 var(--preview-text), 16px 0 0 var(--preview-text);
|
||||||
|
}
|
||||||
|
.preview-shape-bubble .launcher-preview-icon::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
bottom: -8px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-left: 3px solid var(--preview-text);
|
||||||
|
border-bottom: 3px solid var(--preview-text);
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
transform: rotate(-18deg);
|
||||||
|
}
|
||||||
|
.preview-shape-messenger .launcher-preview-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--preview-text);
|
||||||
|
}
|
||||||
|
.preview-shape-messenger .launcher-preview-icon::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 8px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--preview-brand);
|
||||||
|
clip-path: polygon(0 56%, 42% 56%, 58% 22%, 100% 22%, 58% 78%, 42% 78%);
|
||||||
|
}
|
||||||
|
.preview-shape-messenger .launcher-preview-icon::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 1px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 0 0 8px 0;
|
||||||
|
background: var(--preview-text);
|
||||||
|
transform: rotate(25deg);
|
||||||
|
}
|
||||||
|
.preview-shape-whatsapp .launcher-preview-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid var(--preview-text);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.preview-shape-whatsapp .launcher-preview-icon::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 7px;
|
||||||
|
width: 12px;
|
||||||
|
height: 15px;
|
||||||
|
border: 3px solid var(--preview-text);
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 0 0 12px 0;
|
||||||
|
transform: rotate(-35deg);
|
||||||
|
}
|
||||||
|
.preview-shape-whatsapp .launcher-preview-icon::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 1px;
|
||||||
|
bottom: -4px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-left: 3px solid var(--preview-text);
|
||||||
|
border-bottom: 3px solid var(--preview-text);
|
||||||
|
background: transparent;
|
||||||
|
transform: rotate(12deg);
|
||||||
|
}
|
||||||
|
@keyframes launcherPreviewBreathe {
|
||||||
|
0%, 100% { transform: scale(1); box-shadow: 0 12px 30px rgb(0 0 0 / 22%); }
|
||||||
|
50% { transform: scale(1.035); box-shadow: 0 16px 36px rgb(0 0 0 / 20%), 0 0 0 8px rgb(15 143 111 / 10%); }
|
||||||
|
}
|
||||||
|
@keyframes launcherPreviewRing {
|
||||||
|
0% { transform: scale(.92); opacity: .58; }
|
||||||
|
70% { transform: scale(1.22); opacity: 0; }
|
||||||
|
100% { transform: scale(.92); opacity: 0; }
|
||||||
|
}
|
||||||
|
@keyframes launcherPreviewPulse {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgb(255 59 48 / 46%); opacity: .9; transform: scale(1); }
|
||||||
|
70% { box-shadow: 0 0 0 18px rgb(255 59 48 / 0%); opacity: 0; transform: scale(1.08); }
|
||||||
|
100% { box-shadow: 0 0 0 0 rgb(255 59 48 / 0%); opacity: 0; transform: scale(1); }
|
||||||
|
}
|
||||||
|
@keyframes launcherPreviewWiggle {
|
||||||
|
0%, 12%, 100% { transform: translateY(0) rotate(0deg); }
|
||||||
|
3% { transform: translateY(-3px) rotate(-4deg); }
|
||||||
|
6% { transform: translateY(2px) rotate(3deg); }
|
||||||
|
9% { transform: translateY(-1px) rotate(-2deg); }
|
||||||
|
}
|
||||||
.widget-language-settings {
|
.widget-language-settings {
|
||||||
grid-template-columns: 112px minmax(230px, 1fr);
|
grid-template-columns: 112px minmax(230px, 1fr);
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-1
@@ -145,7 +145,7 @@
|
|||||||
<label>Info/GDPR <textarea name="copyInfoText" rows="2"></textarea></label>
|
<label>Info/GDPR <textarea name="copyInfoText" rows="2"></textarea></label>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<button type="submit">Uložit a přeložit</button>
|
<button type="submit">Uložit a přeložit</button>
|
||||||
<span id="settingsSaveStatus" class="settings-save-status" aria-live="polite"></span>
|
<span id="settingsSaveStatus" class="settings-save-status" data-settings-save-status aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<label class="color-field">Barva
|
<label class="color-field">Barva
|
||||||
@@ -173,6 +173,17 @@
|
|||||||
<label><input name="launcherBreathe" type="checkbox"> jemne dychani</label>
|
<label><input name="launcherBreathe" type="checkbox"> jemne dychani</label>
|
||||||
<label><input name="launcherRing" type="checkbox"> svetelny prstenec</label>
|
<label><input name="launcherRing" type="checkbox"> svetelny prstenec</label>
|
||||||
<label><input name="launcherHoverLabel" type="checkbox"> napis pri najeti</label>
|
<label><input name="launcherHoverLabel" type="checkbox"> napis pri najeti</label>
|
||||||
|
<label><input name="launcherStatusDot" type="checkbox"> stavova tecka</label>
|
||||||
|
<label><input name="launcherPersistentLabel" type="checkbox"> stitek stale</label>
|
||||||
|
<label><input name="launcherSway" type="checkbox"> zhoupnuti</label>
|
||||||
|
<div class="launcher-preview-stage">
|
||||||
|
<button id="launcherPreview" class="launcher-preview" type="button" aria-label="Nahled online tlacitka">
|
||||||
|
<span class="launcher-preview-status" aria-hidden="true"></span>
|
||||||
|
<span class="launcher-preview-icon" aria-hidden="true"></span>
|
||||||
|
<span class="launcher-preview-label">Napiste nam</span>
|
||||||
|
<span class="launcher-preview-badge" aria-hidden="true">1</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="effect-settings">
|
<fieldset class="effect-settings">
|
||||||
<legend>Efekt pri nove zprave</legend>
|
<legend>Efekt pri nove zprave</legend>
|
||||||
@@ -180,7 +191,18 @@
|
|||||||
<label><input name="messageBadge" type="checkbox"> badge s poctem</label>
|
<label><input name="messageBadge" type="checkbox"> badge s poctem</label>
|
||||||
<label><input name="messageLabel" type="checkbox"> kratky stitek</label>
|
<label><input name="messageLabel" type="checkbox"> kratky stitek</label>
|
||||||
<label><input name="messageWiggle" type="checkbox"> zhoupnuti</label>
|
<label><input name="messageWiggle" type="checkbox"> zhoupnuti</label>
|
||||||
|
<div class="launcher-preview-stage">
|
||||||
|
<button id="messageEffectPreview" class="launcher-preview" type="button" aria-label="Nahled efektu nove zpravy">
|
||||||
|
<span class="launcher-preview-icon" aria-hidden="true"></span>
|
||||||
|
<span class="launcher-preview-label">Nova odpoved</span>
|
||||||
|
<span class="launcher-preview-badge" aria-hidden="true">1</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<div class="settings-actions">
|
||||||
|
<button type="submit">Ulozit widget</button>
|
||||||
|
<span class="settings-save-status" data-settings-save-status aria-live="polite"></span>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<h3>GTM snippet</h3>
|
<h3>GTM snippet</h3>
|
||||||
<pre id="snippet"></pre>
|
<pre id="snippet"></pre>
|
||||||
|
|||||||
+68
-6
@@ -130,14 +130,27 @@ document.querySelectorAll("[data-settings-tab]").forEach((button) => {
|
|||||||
});
|
});
|
||||||
$("#settingsForm").brand.addEventListener("input", (event) => {
|
$("#settingsForm").brand.addEventListener("input", (event) => {
|
||||||
$("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value;
|
$("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value;
|
||||||
|
renderLauncherPreview();
|
||||||
});
|
});
|
||||||
$("#settingsForm").brandHex.addEventListener("input", (event) => {
|
$("#settingsForm").brandHex.addEventListener("input", (event) => {
|
||||||
const color = normalizeHexColor(event.target.value);
|
const color = normalizeHexColor(event.target.value);
|
||||||
if (color) $("#settingsForm").brand.value = color;
|
if (color) $("#settingsForm").brand.value = color;
|
||||||
|
renderLauncherPreview();
|
||||||
});
|
});
|
||||||
$("#settingsForm").defaultLanguage.addEventListener("change", () => {
|
$("#settingsForm").defaultLanguage.addEventListener("change", () => {
|
||||||
updateDefaultLanguagePicker();
|
updateDefaultLanguagePicker();
|
||||||
renderWidgetCopyFields($("#settingsForm").defaultLanguage.value);
|
renderWidgetCopyFields($("#settingsForm").defaultLanguage.value);
|
||||||
|
renderLauncherPreview();
|
||||||
|
});
|
||||||
|
$("#settingsForm").addEventListener("input", (event) => {
|
||||||
|
if (event.target.matches("[name='launcherShape'], [name='launcherBreathe'], [name='launcherRing'], [name='launcherHoverLabel'], [name='launcherStatusDot'], [name='launcherPersistentLabel'], [name='launcherSway'], [name='copyOnlineLabel'], [name='messagePulse'], [name='messageBadge'], [name='messageLabel'], [name='messageWiggle'], [name='copyNewReplyLabel']")) {
|
||||||
|
renderLauncherPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("#settingsForm").addEventListener("change", (event) => {
|
||||||
|
if (event.target.matches("[name='launcherShape'], [name='launcherBreathe'], [name='launcherRing'], [name='launcherHoverLabel'], [name='launcherStatusDot'], [name='launcherPersistentLabel'], [name='launcherSway'], [name='messagePulse'], [name='messageBadge'], [name='messageLabel'], [name='messageWiggle']")) {
|
||||||
|
renderLauncherPreview();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$("#defaultLanguagePicker").addEventListener("click", (event) => {
|
$("#defaultLanguagePicker").addEventListener("click", (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -168,7 +181,7 @@ $("#onlineToggle").addEventListener("change", async () => {
|
|||||||
|
|
||||||
$("#settingsForm").addEventListener("submit", async (event) => {
|
$("#settingsForm").addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const submitButton = event.currentTarget.querySelector("button[type='submit']");
|
const submitButtons = [...event.currentTarget.querySelectorAll("button[type='submit']")];
|
||||||
setSettingsSaveStatus("saving", "Ukládám...");
|
setSettingsSaveStatus("saving", "Ukládám...");
|
||||||
const form = new FormData(event.currentTarget);
|
const form = new FormData(event.currentTarget);
|
||||||
const settings = structuredClone(state.site.settings);
|
const settings = structuredClone(state.site.settings);
|
||||||
@@ -208,14 +221,19 @@ $("#settingsForm").addEventListener("submit", async (event) => {
|
|||||||
shape: form.get("launcherShape") || "bubble",
|
shape: form.get("launcherShape") || "bubble",
|
||||||
breathe: form.get("launcherBreathe") === "on",
|
breathe: form.get("launcherBreathe") === "on",
|
||||||
ring: form.get("launcherRing") === "on",
|
ring: form.get("launcherRing") === "on",
|
||||||
hoverLabel: form.get("launcherHoverLabel") === "on"
|
hoverLabel: form.get("launcherHoverLabel") === "on",
|
||||||
|
statusDot: form.get("launcherStatusDot") === "on",
|
||||||
|
persistentLabel: form.get("launcherPersistentLabel") === "on",
|
||||||
|
sway: form.get("launcherSway") === "on"
|
||||||
};
|
};
|
||||||
settings.notifications = settings.notifications || {};
|
settings.notifications = settings.notifications || {};
|
||||||
settings.notifications.pulseOnAdminReply = form.get("messagePulse") === "on";
|
settings.notifications.pulseOnAdminReply = form.get("messagePulse") === "on";
|
||||||
settings.notifications.badgeOnAdminReply = form.get("messageBadge") === "on";
|
settings.notifications.badgeOnAdminReply = form.get("messageBadge") === "on";
|
||||||
settings.notifications.labelOnAdminReply = form.get("messageLabel") === "on";
|
settings.notifications.labelOnAdminReply = form.get("messageLabel") === "on";
|
||||||
settings.notifications.wiggleOnAdminReply = form.get("messageWiggle") === "on";
|
settings.notifications.wiggleOnAdminReply = form.get("messageWiggle") === "on";
|
||||||
submitButton.disabled = true;
|
submitButtons.forEach((button) => {
|
||||||
|
button.disabled = true;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
setSettingsSaveStatus("translating", "Překládám...");
|
setSettingsSaveStatus("translating", "Překládám...");
|
||||||
const data = await saveSite({ settings, isOnline: state.site.isOnline });
|
const data = await saveSite({ settings, isOnline: state.site.isOnline });
|
||||||
@@ -232,7 +250,9 @@ $("#settingsForm").addEventListener("submit", async (event) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSettingsSaveStatus("error", "Akce se nepodařila.");
|
setSettingsSaveStatus("error", "Akce se nepodařila.");
|
||||||
} finally {
|
} finally {
|
||||||
submitButton.disabled = false;
|
submitButtons.forEach((button) => {
|
||||||
|
button.disabled = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -519,6 +539,9 @@ function renderSite() {
|
|||||||
form.launcherBreathe.checked = s.launcherEffects?.breathe !== false;
|
form.launcherBreathe.checked = s.launcherEffects?.breathe !== false;
|
||||||
form.launcherRing.checked = s.launcherEffects?.ring !== false;
|
form.launcherRing.checked = s.launcherEffects?.ring !== false;
|
||||||
form.launcherHoverLabel.checked = s.launcherEffects?.hoverLabel !== false;
|
form.launcherHoverLabel.checked = s.launcherEffects?.hoverLabel !== false;
|
||||||
|
form.launcherStatusDot.checked = s.launcherEffects?.statusDot !== false;
|
||||||
|
form.launcherPersistentLabel.checked = Boolean(s.launcherEffects?.persistentLabel);
|
||||||
|
form.launcherSway.checked = Boolean(s.launcherEffects?.sway);
|
||||||
form.messagePulse.checked = s.notifications?.pulseOnAdminReply !== false;
|
form.messagePulse.checked = s.notifications?.pulseOnAdminReply !== false;
|
||||||
form.messageBadge.checked = s.notifications?.badgeOnAdminReply !== false;
|
form.messageBadge.checked = s.notifications?.badgeOnAdminReply !== false;
|
||||||
form.messageLabel.checked = s.notifications?.labelOnAdminReply !== false;
|
form.messageLabel.checked = s.notifications?.labelOnAdminReply !== false;
|
||||||
@@ -532,6 +555,7 @@ function renderSite() {
|
|||||||
renderModelOptions(s.translations?.model || "gpt-5-mini");
|
renderModelOptions(s.translations?.model || "gpt-5-mini");
|
||||||
renderPresenceSettings(s.adminPresence || {});
|
renderPresenceSettings(s.adminPresence || {});
|
||||||
renderTelegramSettings(s.telegram || {});
|
renderTelegramSettings(s.telegram || {});
|
||||||
|
renderLauncherPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWidgetLanguageOptions(form, settings) {
|
function renderWidgetLanguageOptions(form, settings) {
|
||||||
@@ -621,6 +645,44 @@ function renderWidgetCopyFields(language) {
|
|||||||
form.copyInfoText.value = copy.infoText || "";
|
form.copyInfoText.value = copy.infoText || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderLauncherPreview() {
|
||||||
|
const preview = $("#launcherPreview");
|
||||||
|
const messagePreview = $("#messageEffectPreview");
|
||||||
|
const form = $("#settingsForm");
|
||||||
|
if (!preview || !messagePreview || !form) return;
|
||||||
|
const shape = ["circle", "bubble", "messenger", "whatsapp"].includes(form.launcherShape.value)
|
||||||
|
? form.launcherShape.value
|
||||||
|
: "bubble";
|
||||||
|
const brand = normalizeHexColor(form.brandHex.value) || normalizeHexColor(form.brand.value) || "#0f8f6f";
|
||||||
|
const baseClasses = [
|
||||||
|
"launcher-preview",
|
||||||
|
`preview-shape-${shape}`
|
||||||
|
];
|
||||||
|
preview.style.setProperty("--preview-brand", brand);
|
||||||
|
messagePreview.style.setProperty("--preview-brand", brand);
|
||||||
|
preview.className = [
|
||||||
|
...baseClasses,
|
||||||
|
$("#onlineToggle").checked ? "online" : "",
|
||||||
|
form.launcherBreathe.checked ? "effect-breathe" : "",
|
||||||
|
form.launcherRing.checked ? "effect-ring" : "",
|
||||||
|
form.launcherStatusDot.checked ? "show-status" : "",
|
||||||
|
form.launcherPersistentLabel.checked ? "label-always" : "",
|
||||||
|
form.launcherSway.checked ? "effect-sway" : "",
|
||||||
|
form.launcherHoverLabel.checked ? "show-label" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
messagePreview.className = [
|
||||||
|
...baseClasses,
|
||||||
|
form.messagePulse.checked ? "notify-pulse" : "",
|
||||||
|
form.messageBadge.checked ? "show-badge" : "",
|
||||||
|
form.messageLabel.checked ? "show-label" : "",
|
||||||
|
form.messageWiggle.checked ? "notify-wiggle" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
const label = preview.querySelector(".launcher-preview-label");
|
||||||
|
if (label) label.textContent = form.copyOnlineLabel.value || "Napiste nam";
|
||||||
|
const messageLabel = messagePreview.querySelector(".launcher-preview-label");
|
||||||
|
if (messageLabel) messageLabel.textContent = form.copyNewReplyLabel.value || "Nova odpoved";
|
||||||
|
}
|
||||||
|
|
||||||
function widgetCopyForAdmin(settings, language) {
|
function widgetCopyForAdmin(settings, language) {
|
||||||
const lang = language || "cs";
|
const lang = language || "cs";
|
||||||
const copy = settings.widgetCopy?.[lang] || {};
|
const copy = settings.widgetCopy?.[lang] || {};
|
||||||
@@ -1615,11 +1677,11 @@ async function saveSite(patch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSettingsSaveStatus(type, text) {
|
function setSettingsSaveStatus(type, text) {
|
||||||
const status = $("#settingsSaveStatus");
|
for (const status of document.querySelectorAll("[data-settings-save-status]")) {
|
||||||
if (!status) return;
|
|
||||||
status.className = `settings-save-status ${type || ""}`.trim();
|
status.className = `settings-save-status ${type || ""}`.trim();
|
||||||
status.textContent = text || "";
|
status.textContent = text || "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function notify() {
|
function notify() {
|
||||||
startAttention();
|
startAttention();
|
||||||
|
|||||||
+18
-10
@@ -33,6 +33,7 @@
|
|||||||
<style>${styles(settings)}</style>
|
<style>${styles(settings)}</style>
|
||||||
<button class="mf-launcher ${launcherEffectClasses(settings, site.isOnline)}" type="button" aria-label="${escapeHtml(copy.title)}">
|
<button class="mf-launcher ${launcherEffectClasses(settings, site.isOnline)}" type="button" aria-label="${escapeHtml(copy.title)}">
|
||||||
<span class="mf-reply-pulse" aria-hidden="true"></span>
|
<span class="mf-reply-pulse" aria-hidden="true"></span>
|
||||||
|
<span class="mf-status-dot" aria-hidden="true"></span>
|
||||||
<span class="mf-launcher-icon"></span>
|
<span class="mf-launcher-icon"></span>
|
||||||
<span class="mf-launcher-label">${escapeHtml(site.isOnline ? copy.onlineLabel : copy.offlineLabel)}</span>
|
<span class="mf-launcher-label">${escapeHtml(site.isOnline ? copy.onlineLabel : copy.offlineLabel)}</span>
|
||||||
<span class="mf-message-badge" hidden>0</span>
|
<span class="mf-message-badge" hidden>0</span>
|
||||||
@@ -546,6 +547,9 @@
|
|||||||
isOnline ? "mf-online" : "",
|
isOnline ? "mf-online" : "",
|
||||||
isOnline && effects.breathe ? "mf-effect-breathe" : "",
|
isOnline && effects.breathe ? "mf-effect-breathe" : "",
|
||||||
isOnline && effects.ring ? "mf-effect-ring" : "",
|
isOnline && effects.ring ? "mf-effect-ring" : "",
|
||||||
|
effects.statusDot ? "mf-effect-status-dot" : "",
|
||||||
|
effects.persistentLabel ? "mf-effect-persistent-label" : "",
|
||||||
|
isOnline && effects.sway ? "mf-effect-sway" : "",
|
||||||
effects.hoverLabel ? "mf-effect-hover-label" : "",
|
effects.hoverLabel ? "mf-effect-hover-label" : "",
|
||||||
].filter(Boolean).join(" ");
|
].filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
@@ -556,6 +560,7 @@
|
|||||||
|
|
||||||
function applyAdminReplyEffects(launcher, settings, unreadCount) {
|
function applyAdminReplyEffects(launcher, settings, unreadCount) {
|
||||||
const notifications = settings.notifications || {};
|
const notifications = settings.notifications || {};
|
||||||
|
launcher.classList.add("mf-has-message-alert");
|
||||||
if (notifications.pulseOnAdminReply !== false) {
|
if (notifications.pulseOnAdminReply !== false) {
|
||||||
launcher.classList.remove("mf-notify");
|
launcher.classList.remove("mf-notify");
|
||||||
void launcher.offsetWidth;
|
void launcher.offsetWidth;
|
||||||
@@ -565,21 +570,17 @@
|
|||||||
if (notifications.labelOnAdminReply !== false) {
|
if (notifications.labelOnAdminReply !== false) {
|
||||||
launcher.classList.add("mf-show-message-label");
|
launcher.classList.add("mf-show-message-label");
|
||||||
clearTimeout(applyAdminReplyEffects.labelTimer);
|
clearTimeout(applyAdminReplyEffects.labelTimer);
|
||||||
applyAdminReplyEffects.labelTimer = setTimeout(() => launcher.classList.remove("mf-show-message-label"), 4200);
|
|
||||||
}
|
}
|
||||||
if (notifications.wiggleOnAdminReply !== false) {
|
if (notifications.wiggleOnAdminReply !== false) {
|
||||||
launcher.classList.remove("mf-wiggle");
|
launcher.classList.remove("mf-wiggle");
|
||||||
void launcher.offsetWidth;
|
void launcher.offsetWidth;
|
||||||
launcher.classList.add("mf-wiggle");
|
launcher.classList.add("mf-wiggle");
|
||||||
clearTimeout(applyAdminReplyEffects.wiggleTimer);
|
|
||||||
applyAdminReplyEffects.wiggleTimer = setTimeout(() => launcher.classList.remove("mf-wiggle"), 1400);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMessageEffects(launcher) {
|
function clearMessageEffects(launcher) {
|
||||||
launcher.classList.remove("mf-notify", "mf-wiggle", "mf-show-message-label");
|
launcher.classList.remove("mf-notify", "mf-wiggle", "mf-show-message-label", "mf-has-message-alert");
|
||||||
clearTimeout(applyAdminReplyEffects.labelTimer);
|
clearTimeout(applyAdminReplyEffects.labelTimer);
|
||||||
clearTimeout(applyAdminReplyEffects.wiggleTimer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessageBadge(launcher, unreadCount) {
|
function updateMessageBadge(launcher, unreadCount) {
|
||||||
@@ -638,16 +639,23 @@
|
|||||||
}
|
}
|
||||||
.mf-launcher-label { position: absolute; ${labelOffset} top: 50%; transform: translateY(-50%) translateX(8px); opacity: 0; pointer-events: none; white-space: nowrap; border-radius: 999px; padding: 7px 10px; background: ${c.text}; color: white; font-size: 12px; box-shadow: 0 10px 24px rgb(0 0 0 / 18%); transition: opacity .18s ease, transform .18s ease; }
|
.mf-launcher-label { position: absolute; ${labelOffset} top: 50%; transform: translateY(-50%) translateX(8px); opacity: 0; pointer-events: none; white-space: nowrap; border-radius: 999px; padding: 7px 10px; background: ${c.text}; color: white; font-size: 12px; box-shadow: 0 10px 24px rgb(0 0 0 / 18%); transition: opacity .18s ease, transform .18s ease; }
|
||||||
.mf-online.mf-effect-hover-label:hover .mf-launcher-label { opacity: 1; transform: translateY(-50%) translateX(0); }
|
.mf-online.mf-effect-hover-label:hover .mf-launcher-label { opacity: 1; transform: translateY(-50%) translateX(0); }
|
||||||
|
.mf-effect-persistent-label .mf-launcher-label { opacity: 1; transform: translateY(-50%) translateX(0); }
|
||||||
|
.mf-has-message-alert .mf-launcher-label { opacity: 0; transform: translateY(-50%) translateX(8px); }
|
||||||
.mf-message-label { position: absolute; ${labelOffset} top: 50%; transform: translateY(-50%) translateX(8px); opacity: 0; pointer-events: none; white-space: nowrap; border-radius: 999px; padding: 7px 10px; background: ${c.text}; color: white; font-size: 12px; font-weight: 700; box-shadow: 0 10px 24px rgb(0 0 0 / 18%); transition: opacity .18s ease, transform .18s ease; }
|
.mf-message-label { position: absolute; ${labelOffset} top: 50%; transform: translateY(-50%) translateX(8px); opacity: 0; pointer-events: none; white-space: nowrap; border-radius: 999px; padding: 7px 10px; background: ${c.text}; color: white; font-size: 12px; font-weight: 700; box-shadow: 0 10px 24px rgb(0 0 0 / 18%); transition: opacity .18s ease, transform .18s ease; }
|
||||||
.mf-show-message-label .mf-message-label { opacity: 1; transform: translateY(-50%) translateX(0); }
|
.mf-show-message-label .mf-message-label { opacity: 1; transform: translateY(-50%) translateX(0); }
|
||||||
|
.mf-status-dot { position: absolute; top: 4px; right: 4px; width: 14px; height: 14px; display: none; border: 3px solid white; border-radius: 50%; background: #c9d4ce; box-shadow: 0 2px 6px rgb(0 0 0 / 14%); }
|
||||||
|
.mf-effect-status-dot .mf-status-dot { display: block; }
|
||||||
|
.mf-has-message-alert .mf-status-dot { display: none; }
|
||||||
|
.mf-online .mf-status-dot { background: #2f9e44; }
|
||||||
.mf-message-badge { position: absolute; top: -5px; right: -5px; min-width: 22px; height: 22px; display: grid; place-items: center; border-radius: 999px; border: 2px solid white; background: #ff3b30; color: white; font-size: 12px; font-weight: 800; line-height: 1; box-shadow: 0 8px 18px rgb(0 0 0 / 18%); }
|
.mf-message-badge { position: absolute; top: -5px; right: -5px; min-width: 22px; height: 22px; display: grid; place-items: center; border-radius: 999px; border: 2px solid white; background: #ff3b30; color: white; font-size: 12px; font-weight: 800; line-height: 1; box-shadow: 0 8px 18px rgb(0 0 0 / 18%); }
|
||||||
.mf-message-badge[hidden] { display: none !important; }
|
.mf-message-badge[hidden] { display: none !important; }
|
||||||
.mf-reply-pulse { position: absolute; inset: 0; border-radius: 50%; pointer-events: none; opacity: 0; }
|
.mf-reply-pulse { position: absolute; inset: 0; border-radius: 50%; pointer-events: none; opacity: 0; }
|
||||||
.mf-online.mf-effect-breathe { animation: mfBreathe 2.8s ease-in-out infinite; }
|
.mf-online.mf-effect-breathe { animation: mfBreathe 2.8s ease-in-out infinite; }
|
||||||
.mf-online.mf-effect-ring:before { position: absolute; inset: -5px; border: 2px solid ${c.brand}; border-radius: 50%; opacity: .62; content: ""; animation: mfRing 2.2s ease-out infinite; }
|
.mf-online.mf-effect-ring:before { position: absolute; inset: -5px; border: 2px solid ${c.brand}; border-radius: 50%; opacity: .62; content: ""; animation: mfRing 2.2s ease-out infinite; }
|
||||||
|
.mf-online.mf-effect-sway .mf-launcher-icon { animation: mfWiggle 7.2s ease-in-out infinite; }
|
||||||
.mf-offline { filter: grayscale(.2) saturate(.72); opacity: .86; }
|
.mf-offline { filter: grayscale(.2) saturate(.72); opacity: .86; }
|
||||||
.mf-notify .mf-reply-pulse { animation: mfPulse 1.15s ease-out infinite; }
|
.mf-notify .mf-reply-pulse { animation: mfPulse 1.15s ease-out infinite; }
|
||||||
.mf-wiggle .mf-launcher-icon { animation: mfWiggle .62s ease-in-out 2; }
|
.mf-wiggle .mf-launcher-icon { animation: mfWiggle 7.2s ease-in-out infinite; }
|
||||||
.mf-panel {
|
.mf-panel {
|
||||||
position: fixed; z-index: 2147483001; ${desktop.side}: ${desktop.sideOffsetPx}px; bottom: ${desktop.bottomPx + 72}px;
|
position: fixed; z-index: 2147483001; ${desktop.side}: ${desktop.sideOffsetPx}px; bottom: ${desktop.bottomPx + 72}px;
|
||||||
width: min(${desktop.widthPx}px, calc(100vw - 24px)); max-height: min(680px, calc(100vh - 110px)); display: none;
|
width: min(${desktop.widthPx}px, calc(100vw - 24px)); max-height: min(680px, calc(100vh - 110px)); display: none;
|
||||||
@@ -704,10 +712,10 @@
|
|||||||
100% { transform: scale(.92); opacity: 0; }
|
100% { transform: scale(.92); opacity: 0; }
|
||||||
}
|
}
|
||||||
@keyframes mfWiggle {
|
@keyframes mfWiggle {
|
||||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
0%, 12%, 100% { transform: translateY(0) rotate(0deg); }
|
||||||
22% { transform: translateY(-3px) rotate(-4deg); }
|
3% { transform: translateY(-3px) rotate(-4deg); }
|
||||||
48% { transform: translateY(2px) rotate(3deg); }
|
6% { transform: translateY(2px) rotate(3deg); }
|
||||||
72% { transform: translateY(-1px) rotate(-2deg); }
|
9% { transform: translateY(-1px) rotate(-2deg); }
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.mf-launcher {
|
.mf-launcher {
|
||||||
|
|||||||
@@ -341,6 +341,9 @@ function defaultSettings() {
|
|||||||
breathe: true,
|
breathe: true,
|
||||||
ring: true,
|
ring: true,
|
||||||
hoverLabel: true,
|
hoverLabel: true,
|
||||||
|
statusDot: true,
|
||||||
|
persistentLabel: false,
|
||||||
|
sway: false,
|
||||||
avatar: false
|
avatar: false
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
@@ -1111,13 +1114,16 @@ async function translateStoredMessage(conversationId, messageId, senderType, bod
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await translateText(message.body, targetLanguage, settings.model, settings.openaiApiKey);
|
const result = await translateText(message.body, targetLanguage, settings.model, settings.openaiApiKey);
|
||||||
const sourceLanguage = normalizeLanguage(result.sourceLanguage);
|
const detectedSourceLanguage = normalizeDetectedLanguage(result.sourceLanguage);
|
||||||
|
const sourceLanguage = senderType === "visitor"
|
||||||
|
? stableVisitorLanguage(conversation, message.body, detectedSourceLanguage)
|
||||||
|
: detectedSourceLanguage;
|
||||||
const translatedText = String(result.translatedText || "").trim();
|
const translatedText = String(result.translatedText || "").trim();
|
||||||
const translatedLanguage = normalizeLanguage(result.translatedLanguage || targetLanguage);
|
const translatedLanguage = normalizeDetectedLanguage(result.translatedLanguage || targetLanguage);
|
||||||
const sameLanguage = sourceLanguage && translatedLanguage && sourceLanguage === translatedLanguage;
|
const sameLanguage = sourceLanguage && translatedLanguage && sourceLanguage === translatedLanguage;
|
||||||
|
|
||||||
if (senderType === "visitor" && sourceLanguage) {
|
if (senderType === "visitor" && sourceLanguage) {
|
||||||
db.prepare("UPDATE conversations SET customer_language = COALESCE(?, customer_language), updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(sourceLanguage, conversationId);
|
db.prepare("UPDATE conversations SET customer_language = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(sourceLanguage, conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!translatedText || sameLanguage) {
|
if (!translatedText || sameLanguage) {
|
||||||
@@ -1154,7 +1160,7 @@ async function translateText(text, targetLanguage, model, apiKey) {
|
|||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: "You translate live chat messages for an e-commerce support team. Return only compact JSON with keys sourceLanguage, translatedLanguage, translatedText. Preserve meaning, product terms, numbers, names, URLs, and tone. Do not answer the customer."
|
content: "You translate live chat messages for an e-commerce support team. Return only compact JSON with keys sourceLanguage, translatedLanguage, translatedText. Preserve meaning, product terms, numbers, names, URLs, and tone. Do not answer the customer. If the source language is unclear because the text is a greeting, product name, SKU, brand, color, URL, or otherwise language-neutral, set sourceLanguage to unknown."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -1765,6 +1771,53 @@ function normalizeLanguage(value) {
|
|||||||
return text.split("-")[0].slice(0, 8);
|
return text.split("-")[0].slice(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDetectedLanguage(value) {
|
||||||
|
const language = normalizeLanguage(value);
|
||||||
|
return ["unknown", "und", "auto", "n/a", "none"].includes(language) ? "" : language;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableVisitorLanguage(conversation, text, detectedLanguage) {
|
||||||
|
const currentLanguage = normalizeDetectedLanguage(conversation?.customer_language);
|
||||||
|
const browserLanguage = normalizeDetectedLanguage(conversation?.language);
|
||||||
|
if (!currentLanguage) {
|
||||||
|
if (browserLanguage && weakLanguageEvidence(text)) return browserLanguage;
|
||||||
|
return detectedLanguage;
|
||||||
|
}
|
||||||
|
if (!detectedLanguage || detectedLanguage === currentLanguage) return currentLanguage;
|
||||||
|
return weakLanguageEvidence(text) ? currentLanguage : detectedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function weakLanguageEvidence(value) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
if (!text) return true;
|
||||||
|
|
||||||
|
const words = text.match(/[\p{L}\p{N}][\p{L}\p{N}'-]*/gu) || [];
|
||||||
|
const lowerWords = words.map((word) => word.toLowerCase());
|
||||||
|
if (!words.length) return true;
|
||||||
|
if (hasStrongLanguageEvidence(text, lowerWords)) return false;
|
||||||
|
|
||||||
|
const compact = lowerWords.join(" ");
|
||||||
|
const weakPhrases = new Set([
|
||||||
|
"hi", "hello", "hey", "ok", "okay", "yes", "no", "thanks", "thank you",
|
||||||
|
"hallo", "servus", "danke", "ja", "nein", "bitte"
|
||||||
|
]);
|
||||||
|
if (weakPhrases.has(compact)) return true;
|
||||||
|
if (words.length <= 2) return true;
|
||||||
|
|
||||||
|
const startsLikeProductName = /^[\p{Lu}\p{N}]/u.test(words[0]);
|
||||||
|
const sentencePunctuation = /[?!.]/.test(text);
|
||||||
|
return words.length <= 6 && startsLikeProductName && !sentencePunctuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasStrongLanguageEvidence(text, lowerWords) {
|
||||||
|
const strongWords = new Set([
|
||||||
|
"i", "you", "we", "need", "want", "would", "could", "please", "what", "which", "size", "have", "has", "is", "are",
|
||||||
|
"ich", "sie", "wir", "brauche", "benotige", "möchte", "mochte", "bitte", "welche", "welcher", "welches", "größe", "groesse", "hat", "haben", "ist", "sind", "wie", "was", "kann"
|
||||||
|
]);
|
||||||
|
const hits = lowerWords.filter((word) => strongWords.has(word)).length;
|
||||||
|
return hits >= 2 || (hits >= 1 && /[?]/.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
function truncateText(value, maxLength) {
|
function truncateText(value, maxLength) {
|
||||||
const text = String(value || "").trim();
|
const text = String(value || "").trim();
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user