Deploy movable Thinkific tutor 5361fe2

#1
Files changed (1) hide show
  1. 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:pointer;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
  ".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'>&minus;</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
- if (state.open) {
391
- setTimeout(function () {
392
- input.focus();
393
- }, 0);
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'>&minus;</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") {