Deploy movable Thinkific tutor 5361fe2
#1
by whatsai - opened
- static/thinkific-widget.js +183 -8
static/thinkific-widget.js
CHANGED
|
@@ -37,11 +37,13 @@
|
|
| 37 |
"<style>",
|
| 38 |
":host{all:initial;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#182235}",
|
| 39 |
".wrap{position:fixed;right:20px;bottom:20px;z-index:2147483000}",
|
| 40 |
-
".bubble{height:46px;min-width:142px;border:0;border-radius:999px;background:#182235;color:#fff;box-shadow:0 14px 34px rgba(24,34,53,.28);cursor:
|
| 41 |
".bubble:hover{background:#24324b;transform:translateY(-1px)}",
|
|
|
|
| 42 |
".bubble-mark{width:24px;height:24px;border-radius:999px;background:#11b7ba;color:#082c33;display:flex;align-items:center;justify-content:center;font:800 15px/1 system-ui}",
|
| 43 |
".panel{width:min(414px,calc(100vw - 32px));height:min(650px,calc(100vh - 96px));background:#fff;border:1px solid #d7e0ea;border-radius:14px;box-shadow:0 20px 70px rgba(24,34,53,.24);display:flex;flex-direction:column;overflow:hidden}",
|
| 44 |
-
".head{min-height:68px;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 14px 0 16px;border-bottom:1px solid #e7edf4;background:#fbfcfe}",
|
|
|
|
| 45 |
".brand{min-width:0;display:flex;align-items:center;gap:10px}",
|
| 46 |
".brand-mark{width:32px;height:32px;border-radius:8px;background:#182235;color:#fff;display:flex;align-items:center;justify-content:center;font:800 12px/1 system-ui}",
|
| 47 |
".title{font:750 15px/1.2 system-ui;color:#182235;letter-spacing:0}",
|
|
@@ -79,7 +81,7 @@
|
|
| 79 |
"<div class='wrap hidden' data-wrap>",
|
| 80 |
" <button class='bubble' data-bubble aria-label='Ask the tutor'><span class='bubble-mark'>?</span><span>Ask the tutor</span></button>",
|
| 81 |
" <section class='panel hidden' data-panel aria-label='AI tutor chat'>",
|
| 82 |
-
" <header class='head'>",
|
| 83 |
" <div class='brand'><div class='brand-mark'>TA</div><div><div class='title'>Towards AI Tutor</div><div class='lesson' data-lesson></div></div></div>",
|
| 84 |
" <div class='actions'><button class='iconbtn' data-reset aria-label='Restart conversation' title='Restart conversation'><svg viewBox='0 0 24 24' aria-hidden='true'><path d='M3 3v6h6' fill='none' stroke='currentColor' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'/><path d='M3.8 13.1a8 8 0 1 0 2.3-7.2L3 9' fill='none' stroke='currentColor' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'/></svg></button><button class='iconbtn' data-close aria-label='Minimize tutor' title='Minimize'>−</button></div>",
|
| 85 |
" </header>",
|
|
@@ -95,6 +97,7 @@
|
|
| 95 |
var wrap = shadow.querySelector("[data-wrap]");
|
| 96 |
var bubble = shadow.querySelector("[data-bubble]");
|
| 97 |
var panel = shadow.querySelector("[data-panel]");
|
|
|
|
| 98 |
var resetBtn = shadow.querySelector("[data-reset]");
|
| 99 |
var closeBtn = shadow.querySelector("[data-close]");
|
| 100 |
var form = shadow.querySelector("[data-form]");
|
|
@@ -102,6 +105,140 @@
|
|
| 102 |
var sendBtn = shadow.querySelector("[data-send]");
|
| 103 |
var msgs = shadow.querySelector("[data-msgs]");
|
| 104 |
var lessonLabel = shadow.querySelector("[data-lesson]");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
function safeString(value) {
|
| 107 |
if (value === null || value === undefined) return "";
|
|
@@ -380,6 +517,10 @@
|
|
| 380 |
wrap.classList.toggle("hidden", !state.visible);
|
| 381 |
if (!state.visible) {
|
| 382 |
setOpen(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}
|
| 384 |
}
|
| 385 |
|
|
@@ -387,11 +528,10 @@
|
|
| 387 |
state.open = Boolean(isOpen);
|
| 388 |
bubble.classList.toggle("hidden", state.open);
|
| 389 |
panel.classList.toggle("hidden", !state.open);
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
}
|
| 395 |
}
|
| 396 |
|
| 397 |
function renderMessages() {
|
|
@@ -583,7 +723,24 @@
|
|
| 583 |
}
|
| 584 |
}
|
| 585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
bubble.addEventListener("click", function () {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
setOpen(true);
|
| 588 |
});
|
| 589 |
resetBtn.addEventListener("click", resetConversation);
|
|
@@ -608,6 +765,24 @@
|
|
| 608 |
form.dispatchEvent(new Event("submit", { cancelable: true }));
|
| 609 |
}
|
| 610 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
|
| 612 |
function attachCoursePlayerHooks() {
|
| 613 |
if (!window.CoursePlayerV2 || typeof window.CoursePlayerV2.on !== "function") {
|
|
|
|
| 37 |
"<style>",
|
| 38 |
":host{all:initial;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#182235}",
|
| 39 |
".wrap{position:fixed;right:20px;bottom:20px;z-index:2147483000}",
|
| 40 |
+
".bubble{height:46px;min-width:142px;border:0;border-radius:999px;background:#182235;color:#fff;box-shadow:0 14px 34px rgba(24,34,53,.28);cursor:grab;touch-action:none;user-select:none;display:flex;align-items:center;justify-content:center;gap:9px;padding:0 16px 0 13px;font:750 14px/1 system-ui;letter-spacing:0}",
|
| 41 |
".bubble:hover{background:#24324b;transform:translateY(-1px)}",
|
| 42 |
+
".wrap.dragging .bubble{cursor:grabbing;transform:none}",
|
| 43 |
".bubble-mark{width:24px;height:24px;border-radius:999px;background:#11b7ba;color:#082c33;display:flex;align-items:center;justify-content:center;font:800 15px/1 system-ui}",
|
| 44 |
".panel{width:min(414px,calc(100vw - 32px));height:min(650px,calc(100vh - 96px));background:#fff;border:1px solid #d7e0ea;border-radius:14px;box-shadow:0 20px 70px rgba(24,34,53,.24);display:flex;flex-direction:column;overflow:hidden}",
|
| 45 |
+
".head{min-height:68px;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 14px 0 16px;border-bottom:1px solid #e7edf4;background:#fbfcfe;cursor:grab;touch-action:none;user-select:none}",
|
| 46 |
+
".wrap.dragging .head{cursor:grabbing}",
|
| 47 |
".brand{min-width:0;display:flex;align-items:center;gap:10px}",
|
| 48 |
".brand-mark{width:32px;height:32px;border-radius:8px;background:#182235;color:#fff;display:flex;align-items:center;justify-content:center;font:800 12px/1 system-ui}",
|
| 49 |
".title{font:750 15px/1.2 system-ui;color:#182235;letter-spacing:0}",
|
|
|
|
| 81 |
"<div class='wrap hidden' data-wrap>",
|
| 82 |
" <button class='bubble' data-bubble aria-label='Ask the tutor'><span class='bubble-mark'>?</span><span>Ask the tutor</span></button>",
|
| 83 |
" <section class='panel hidden' data-panel aria-label='AI tutor chat'>",
|
| 84 |
+
" <header class='head' data-drag-handle title='Drag to move tutor'>",
|
| 85 |
" <div class='brand'><div class='brand-mark'>TA</div><div><div class='title'>Towards AI Tutor</div><div class='lesson' data-lesson></div></div></div>",
|
| 86 |
" <div class='actions'><button class='iconbtn' data-reset aria-label='Restart conversation' title='Restart conversation'><svg viewBox='0 0 24 24' aria-hidden='true'><path d='M3 3v6h6' fill='none' stroke='currentColor' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'/><path d='M3.8 13.1a8 8 0 1 0 2.3-7.2L3 9' fill='none' stroke='currentColor' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round'/></svg></button><button class='iconbtn' data-close aria-label='Minimize tutor' title='Minimize'>−</button></div>",
|
| 87 |
" </header>",
|
|
|
|
| 97 |
var wrap = shadow.querySelector("[data-wrap]");
|
| 98 |
var bubble = shadow.querySelector("[data-bubble]");
|
| 99 |
var panel = shadow.querySelector("[data-panel]");
|
| 100 |
+
var dragHandle = shadow.querySelector("[data-drag-handle]");
|
| 101 |
var resetBtn = shadow.querySelector("[data-reset]");
|
| 102 |
var closeBtn = shadow.querySelector("[data-close]");
|
| 103 |
var form = shadow.querySelector("[data-form]");
|
|
|
|
| 105 |
var sendBtn = shadow.querySelector("[data-send]");
|
| 106 |
var msgs = shadow.querySelector("[data-msgs]");
|
| 107 |
var lessonLabel = shadow.querySelector("[data-lesson]");
|
| 108 |
+
var positionStorageKey = "towards-ai-thinkific-tutor-position-v1";
|
| 109 |
+
var customPosition = false;
|
| 110 |
+
var suppressBubbleClick = false;
|
| 111 |
+
|
| 112 |
+
function readSavedPosition() {
|
| 113 |
+
try {
|
| 114 |
+
var parsed = JSON.parse(window.localStorage.getItem(positionStorageKey) || "null");
|
| 115 |
+
if (
|
| 116 |
+
parsed &&
|
| 117 |
+
Number.isFinite(parsed.left) &&
|
| 118 |
+
Number.isFinite(parsed.top)
|
| 119 |
+
) {
|
| 120 |
+
return { left: parsed.left, top: parsed.top };
|
| 121 |
+
}
|
| 122 |
+
} catch (_error) {}
|
| 123 |
+
return null;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
function viewportSize() {
|
| 127 |
+
return {
|
| 128 |
+
width: Math.max(
|
| 129 |
+
document.documentElement.clientWidth || 0,
|
| 130 |
+
window.innerWidth || 0,
|
| 131 |
+
),
|
| 132 |
+
height: Math.max(
|
| 133 |
+
document.documentElement.clientHeight || 0,
|
| 134 |
+
window.innerHeight || 0,
|
| 135 |
+
),
|
| 136 |
+
};
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
function clampCoordinates(left, top, width, height) {
|
| 140 |
+
var margin = 12;
|
| 141 |
+
var viewport = viewportSize();
|
| 142 |
+
var maxLeft = Math.max(margin, viewport.width - width - margin);
|
| 143 |
+
var maxTop = Math.max(margin, viewport.height - height - margin);
|
| 144 |
+
return {
|
| 145 |
+
left: Math.min(Math.max(left, margin), maxLeft),
|
| 146 |
+
top: Math.min(Math.max(top, margin), maxTop),
|
| 147 |
+
};
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function positionWrap(left, top) {
|
| 151 |
+
wrap.style.left = Math.round(left) + "px";
|
| 152 |
+
wrap.style.top = Math.round(top) + "px";
|
| 153 |
+
wrap.style.right = "auto";
|
| 154 |
+
wrap.style.bottom = "auto";
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
function clampToViewport() {
|
| 158 |
+
if (!customPosition || wrap.classList.contains("hidden")) return;
|
| 159 |
+
var rect = wrap.getBoundingClientRect();
|
| 160 |
+
if (!rect.width || !rect.height) return;
|
| 161 |
+
var clamped = clampCoordinates(rect.left, rect.top, rect.width, rect.height);
|
| 162 |
+
positionWrap(clamped.left, clamped.top);
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function savePosition() {
|
| 166 |
+
if (!customPosition) return;
|
| 167 |
+
var rect = wrap.getBoundingClientRect();
|
| 168 |
+
try {
|
| 169 |
+
window.localStorage.setItem(
|
| 170 |
+
positionStorageKey,
|
| 171 |
+
JSON.stringify({ left: Math.round(rect.left), top: Math.round(rect.top) }),
|
| 172 |
+
);
|
| 173 |
+
} catch (_error) {}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function restorePosition() {
|
| 177 |
+
var saved = readSavedPosition();
|
| 178 |
+
if (!saved) return;
|
| 179 |
+
customPosition = true;
|
| 180 |
+
positionWrap(saved.left, saved.top);
|
| 181 |
+
window.requestAnimationFrame(function () {
|
| 182 |
+
clampToViewport();
|
| 183 |
+
savePosition();
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
function beginDrag(event, options) {
|
| 188 |
+
options = options || {};
|
| 189 |
+
if (!state.visible) return;
|
| 190 |
+
if (event.pointerType === "mouse" && event.button !== 0) return;
|
| 191 |
+
if (
|
| 192 |
+
options.ignoreActions &&
|
| 193 |
+
event.target &&
|
| 194 |
+
typeof event.target.closest === "function" &&
|
| 195 |
+
event.target.closest(".actions")
|
| 196 |
+
) {
|
| 197 |
+
return;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
var rect = wrap.getBoundingClientRect();
|
| 201 |
+
if (!rect.width || !rect.height) return;
|
| 202 |
+
var pointerId = event.pointerId;
|
| 203 |
+
var startX = event.clientX;
|
| 204 |
+
var startY = event.clientY;
|
| 205 |
+
var startLeft = rect.left;
|
| 206 |
+
var startTop = rect.top;
|
| 207 |
+
var moved = false;
|
| 208 |
+
|
| 209 |
+
function onPointerMove(moveEvent) {
|
| 210 |
+
if (moveEvent.pointerId !== pointerId) return;
|
| 211 |
+
var deltaX = moveEvent.clientX - startX;
|
| 212 |
+
var deltaY = moveEvent.clientY - startY;
|
| 213 |
+
if (!moved && Math.abs(deltaX) + Math.abs(deltaY) < 6) return;
|
| 214 |
+
moved = true;
|
| 215 |
+
customPosition = true;
|
| 216 |
+
wrap.classList.add("dragging");
|
| 217 |
+
var clamped = clampCoordinates(
|
| 218 |
+
startLeft + deltaX,
|
| 219 |
+
startTop + deltaY,
|
| 220 |
+
rect.width,
|
| 221 |
+
rect.height,
|
| 222 |
+
);
|
| 223 |
+
positionWrap(clamped.left, clamped.top);
|
| 224 |
+
moveEvent.preventDefault();
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
function finishDrag(endEvent) {
|
| 228 |
+
if (endEvent.pointerId !== pointerId) return;
|
| 229 |
+
window.removeEventListener("pointermove", onPointerMove);
|
| 230 |
+
window.removeEventListener("pointerup", finishDrag);
|
| 231 |
+
window.removeEventListener("pointercancel", finishDrag);
|
| 232 |
+
wrap.classList.remove("dragging");
|
| 233 |
+
if (!moved) return;
|
| 234 |
+
savePosition();
|
| 235 |
+
if (typeof options.onMoved === "function") options.onMoved();
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
window.addEventListener("pointermove", onPointerMove, { passive: false });
|
| 239 |
+
window.addEventListener("pointerup", finishDrag);
|
| 240 |
+
window.addEventListener("pointercancel", finishDrag);
|
| 241 |
+
}
|
| 242 |
|
| 243 |
function safeString(value) {
|
| 244 |
if (value === null || value === undefined) return "";
|
|
|
|
| 517 |
wrap.classList.toggle("hidden", !state.visible);
|
| 518 |
if (!state.visible) {
|
| 519 |
setOpen(false);
|
| 520 |
+
} else if (!customPosition) {
|
| 521 |
+
restorePosition();
|
| 522 |
+
} else {
|
| 523 |
+
window.requestAnimationFrame(clampToViewport);
|
| 524 |
}
|
| 525 |
}
|
| 526 |
|
|
|
|
| 528 |
state.open = Boolean(isOpen);
|
| 529 |
bubble.classList.toggle("hidden", state.open);
|
| 530 |
panel.classList.toggle("hidden", !state.open);
|
| 531 |
+
window.requestAnimationFrame(function () {
|
| 532 |
+
clampToViewport();
|
| 533 |
+
if (state.open) input.focus();
|
| 534 |
+
});
|
|
|
|
| 535 |
}
|
| 536 |
|
| 537 |
function renderMessages() {
|
|
|
|
| 723 |
}
|
| 724 |
}
|
| 725 |
|
| 726 |
+
bubble.addEventListener("pointerdown", function (event) {
|
| 727 |
+
beginDrag(event, {
|
| 728 |
+
onMoved: function () {
|
| 729 |
+
suppressBubbleClick = true;
|
| 730 |
+
window.setTimeout(function () {
|
| 731 |
+
suppressBubbleClick = false;
|
| 732 |
+
}, 0);
|
| 733 |
+
},
|
| 734 |
+
});
|
| 735 |
+
});
|
| 736 |
+
dragHandle.addEventListener("pointerdown", function (event) {
|
| 737 |
+
beginDrag(event, { ignoreActions: true });
|
| 738 |
+
});
|
| 739 |
bubble.addEventListener("click", function () {
|
| 740 |
+
if (suppressBubbleClick) {
|
| 741 |
+
suppressBubbleClick = false;
|
| 742 |
+
return;
|
| 743 |
+
}
|
| 744 |
setOpen(true);
|
| 745 |
});
|
| 746 |
resetBtn.addEventListener("click", resetConversation);
|
|
|
|
| 765 |
form.dispatchEvent(new Event("submit", { cancelable: true }));
|
| 766 |
}
|
| 767 |
});
|
| 768 |
+
window.addEventListener("resize", function () {
|
| 769 |
+
if (!customPosition) return;
|
| 770 |
+
window.requestAnimationFrame(function () {
|
| 771 |
+
window.requestAnimationFrame(function () {
|
| 772 |
+
clampToViewport();
|
| 773 |
+
savePosition();
|
| 774 |
+
});
|
| 775 |
+
});
|
| 776 |
+
});
|
| 777 |
+
if (typeof window.ResizeObserver === "function") {
|
| 778 |
+
new window.ResizeObserver(function () {
|
| 779 |
+
if (!customPosition || wrap.classList.contains("hidden")) return;
|
| 780 |
+
window.requestAnimationFrame(function () {
|
| 781 |
+
clampToViewport();
|
| 782 |
+
savePosition();
|
| 783 |
+
});
|
| 784 |
+
}).observe(wrap);
|
| 785 |
+
}
|
| 786 |
|
| 787 |
function attachCoursePlayerHooks() {
|
| 788 |
if (!window.CoursePlayerV2 || typeof window.CoursePlayerV2.on !== "function") {
|