@yassirbenmoussa/aicommerce-sdk 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -105,7 +105,10 @@ var init_client = __esm({
105
105
  */
106
106
  detectBaseUrl() {
107
107
  if (typeof window !== "undefined") {
108
- return window.location.origin;
108
+ const script = document.querySelector("script[data-aicommerce-url]");
109
+ if (script) {
110
+ return script.getAttribute("data-aicommerce-url") || "https://api.aicommerce.dev";
111
+ }
109
112
  }
110
113
  return "https://api.aicommerce.dev";
111
114
  }
@@ -189,6 +192,49 @@ var init_client = __esm({
189
192
  }
190
193
  return response;
191
194
  }
195
+ /**
196
+ * Send an audio message and get product recommendations
197
+ *
198
+ * @param audioBlob - Audio blob (from MediaRecorder or file input)
199
+ * @param context - Optional context for better recommendations
200
+ * @returns Chat response with AI reply and products
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * // Record audio using MediaRecorder
205
+ * const mediaRecorder = new MediaRecorder(stream);
206
+ * const chunks: Blob[] = [];
207
+ * mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
208
+ * mediaRecorder.onstop = async () => {
209
+ * const audioBlob = new Blob(chunks, { type: 'audio/webm' });
210
+ * const response = await client.chatWithAudio(audioBlob);
211
+ * console.log(response.reply);
212
+ * };
213
+ * ```
214
+ */
215
+ async chatWithAudio(audioBlob, context) {
216
+ const arrayBuffer = await audioBlob.arrayBuffer();
217
+ const base64 = btoa(
218
+ new Uint8Array(arrayBuffer).reduce(
219
+ (data, byte) => data + String.fromCharCode(byte),
220
+ ""
221
+ )
222
+ );
223
+ const request = {
224
+ audioBase64: base64,
225
+ audioMimeType: audioBlob.type || "audio/webm",
226
+ context,
227
+ sessionToken: this.sessionToken || void 0
228
+ };
229
+ const response = await this.request("/api/v1/chat", {
230
+ method: "POST",
231
+ body: JSON.stringify(request)
232
+ });
233
+ if (response.sessionToken) {
234
+ this.sessionToken = response.sessionToken;
235
+ }
236
+ return response;
237
+ }
192
238
  /**
193
239
  * Create a new chat session
194
240
  *
@@ -550,15 +596,24 @@ function createWidgetStyles(config) {
550
596
  /* Product Cards */
551
597
  .aicommerce-products {
552
598
  display: flex;
553
- gap: 8px;
599
+ gap: 16px;
554
600
  margin-top: 12px;
555
601
  overflow-x: auto;
556
- padding-bottom: 4px;
602
+ padding-bottom: 16px;
603
+ width: 100%;
604
+ max-width: 100%;
605
+ cursor: grab;
606
+ user-select: none;
607
+ -webkit-user-select: none;
608
+ scrollbar-width: none; /* Firefox */
609
+ }
610
+ .aicommerce-products::-webkit-scrollbar {
611
+ display: none; /* Chrome/Safari */
557
612
  }
558
613
 
559
614
  .aicommerce-product-card {
560
615
  flex-shrink: 0;
561
- width: 140px;
616
+ width: 280px;
562
617
  background: var(--aic-bg);
563
618
  border-radius: 12px;
564
619
  overflow: hidden;
@@ -574,13 +629,15 @@ function createWidgetStyles(config) {
574
629
 
575
630
  .aicommerce-product-image {
576
631
  width: 100%;
577
- height: 100px;
632
+ aspect-ratio: 16/9;
633
+ height: auto;
578
634
  object-fit: cover;
579
635
  }
580
636
 
581
637
  .aicommerce-product-placeholder {
582
638
  width: 100%;
583
- height: 100px;
639
+ aspect-ratio: 16/9;
640
+ height: auto;
584
641
  background: var(--aic-bg-secondary);
585
642
  display: flex;
586
643
  align-items: center;
@@ -610,6 +667,109 @@ function createWidgetStyles(config) {
610
667
  color: var(--aic-primary);
611
668
  }
612
669
 
670
+ .aicommerce-product-desc {
671
+ font-size: 12px;
672
+ color: var(--aic-text-secondary);
673
+ line-height: 1.4;
674
+ display: -webkit-box;
675
+ -webkit-line-clamp: 2;
676
+ -webkit-box-orient: vertical;
677
+ overflow: hidden;
678
+ margin-top: 4px;
679
+ }
680
+
681
+ /* Audio Player */
682
+ .aicommerce-audio-player {
683
+ display: flex;
684
+ align-items: center;
685
+ gap: 12px;
686
+ min-width: 240px;
687
+ padding: 4px 0;
688
+ }
689
+
690
+ .aicommerce-audio-btn {
691
+ width: 40px;
692
+ height: 40px;
693
+ border-radius: 50%;
694
+ display: flex;
695
+ align-items: center;
696
+ justify-content: center;
697
+ border: none;
698
+ cursor: pointer;
699
+ transition: all 0.2s;
700
+ background: rgba(255, 255, 255, 0.25);
701
+ color: white;
702
+ flex-shrink: 0;
703
+ padding: 0;
704
+ }
705
+
706
+ .aicommerce-audio-btn:hover {
707
+ background: rgba(255, 255, 255, 0.35);
708
+ transform: scale(1.05);
709
+ }
710
+
711
+ .aicommerce-audio-btn:active {
712
+ transform: scale(0.95);
713
+ }
714
+
715
+ /* Invert colors for assistant (since background is white/gray) */
716
+ .aicommerce-assistant .aicommerce-audio-btn {
717
+ background: var(--aic-primary);
718
+ color: white;
719
+ }
720
+ .aicommerce-assistant .aicommerce-audio-btn:hover {
721
+ background: var(--aic-primary-dark);
722
+ }
723
+
724
+ .aicommerce-audio-waveform {
725
+ flex: 1;
726
+ display: flex;
727
+ flex-direction: column;
728
+ gap: 6px;
729
+ min-width: 0; /* Prevent overflow */
730
+ }
731
+
732
+ .aicommerce-waveform-bars {
733
+ display: flex;
734
+ align-items: center;
735
+ gap: 2px;
736
+ height: 24px;
737
+ cursor: pointer;
738
+ width: 100%;
739
+ }
740
+
741
+ .aicommerce-waveform-bar {
742
+ width: 3px;
743
+ border-radius: 2px;
744
+ min-height: 3px;
745
+ transition: background-color 0.1s;
746
+ flex-shrink: 0;
747
+ }
748
+
749
+ .aicommerce-audio-time {
750
+ display: flex;
751
+ justify-content: space-between;
752
+ font-size: 11px;
753
+ font-weight: 500;
754
+ }
755
+
756
+ .aicommerce-user .aicommerce-audio-time {
757
+ color: rgba(255, 255, 255, 0.8);
758
+ }
759
+ .aicommerce-assistant .aicommerce-audio-time {
760
+ color: var(--aic-text-secondary);
761
+ }
762
+
763
+ /* RTL Support */
764
+ .aicommerce-rtl {
765
+ direction: rtl;
766
+ text-align: right;
767
+ }
768
+ .aicommerce-ltr {
769
+ direction: ltr;
770
+ text-align: left;
771
+ }
772
+
613
773
  /* Input Area */
614
774
  .aicommerce-input-container {
615
775
  padding: 16px 20px;
@@ -664,6 +824,44 @@ function createWidgetStyles(config) {
664
824
  cursor: not-allowed;
665
825
  }
666
826
 
827
+ /* Microphone Button */
828
+ .aicommerce-mic {
829
+ width: 44px;
830
+ height: 44px;
831
+ border-radius: 50%;
832
+ background: var(--aic-bg-secondary);
833
+ border: 1px solid var(--aic-border);
834
+ color: var(--aic-text-secondary);
835
+ cursor: pointer;
836
+ display: flex;
837
+ align-items: center;
838
+ justify-content: center;
839
+ transition: all 0.2s;
840
+ }
841
+
842
+ .aicommerce-mic:hover:not(:disabled) {
843
+ background: var(--aic-primary-light);
844
+ border-color: var(--aic-primary);
845
+ color: var(--aic-primary);
846
+ }
847
+
848
+ .aicommerce-mic.aicommerce-recording {
849
+ background: #ef4444;
850
+ border-color: #ef4444;
851
+ color: white;
852
+ animation: aic-recording-pulse 1s infinite;
853
+ }
854
+
855
+ .aicommerce-mic:disabled {
856
+ opacity: 0.6;
857
+ cursor: not-allowed;
858
+ }
859
+
860
+ @keyframes aic-recording-pulse {
861
+ 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
862
+ 50% { transform: scale(1.05); box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
863
+ }
864
+
667
865
  /* Mobile Responsive */
668
866
  @media (max-width: 420px) {
669
867
  #aicommerce-widget {
@@ -735,9 +933,12 @@ function createWidget(config) {
735
933
  const state = {
736
934
  isOpen: false,
737
935
  isLoading: true,
936
+ isRecording: false,
738
937
  messages: [],
739
938
  storeConfig: null
740
939
  };
940
+ let mediaRecorder = null;
941
+ let audioChunks = [];
741
942
  let container = null;
742
943
  let styleElement = null;
743
944
  let resolvedConfig;
@@ -820,20 +1021,26 @@ function createWidget(config) {
820
1021
  </div>
821
1022
 
822
1023
  <div class="aicommerce-messages">
823
- ${state.messages.map((msg) => `
1024
+ ${state.messages.map((msg, index) => {
1025
+ const isRtl = isArabic(msg.content);
1026
+ const isUser = msg.role === "user";
1027
+ return `
824
1028
  <div class="aicommerce-message aicommerce-${msg.role}">
825
- <div class="aicommerce-message-content">${escapeHtml(msg.content)}</div>
1029
+ <div class="aicommerce-message-content ${isRtl ? "aicommerce-rtl" : "aicommerce-ltr"}">
1030
+ ${msg.audioUrl ? renderAudioPlayer(msg, index, isUser) : escapeHtml(msg.content)}
1031
+ </div>
826
1032
  ${msg.products && msg.products.length > 0 ? `
827
1033
  <div class="aicommerce-products">
828
1034
  ${msg.products.map((product) => `
829
1035
  <div class="aicommerce-product-card" data-product-id="${product.id}">
830
- ${product.imageUrl ? `
831
- <img src="${product.imageUrl}" alt="${escapeHtml(product.name)}" class="aicommerce-product-image" />
1036
+ ${product.image || product.imageUrl ? `
1037
+ <img src="${product.image || product.imageUrl}" alt="${escapeHtml(product.name)}" class="aicommerce-product-image" />
832
1038
  ` : `
833
1039
  <div class="aicommerce-product-placeholder">\u{1F4E6}</div>
834
1040
  `}
835
1041
  <div class="aicommerce-product-info">
836
- <span class="aicommerce-product-name">${escapeHtml(product.name)}</span>
1042
+ <span class="aicommerce-product-name" title="${escapeHtml(product.name)}">${escapeHtml(product.name)}</span>
1043
+ ${product.description ? `<p class="aicommerce-product-desc">${escapeHtml(product.description)}</p>` : ""}
837
1044
  <span class="aicommerce-product-price">${formatPrice(product.price, product.currency)}</span>
838
1045
  </div>
839
1046
  </div>
@@ -841,7 +1048,8 @@ function createWidget(config) {
841
1048
  </div>
842
1049
  ` : ""}
843
1050
  </div>
844
- `).join("")}
1051
+ `;
1052
+ }).join("")}
845
1053
  ${state.isLoading ? `
846
1054
  <div class="aicommerce-message aicommerce-assistant">
847
1055
  <div class="aicommerce-typing">
@@ -856,9 +1064,23 @@ function createWidget(config) {
856
1064
  type="text"
857
1065
  class="aicommerce-input"
858
1066
  placeholder="Type your message..."
859
- ${state.isLoading ? "disabled" : ""}
1067
+ ${state.isLoading || state.isRecording ? "disabled" : ""}
860
1068
  />
861
- <button class="aicommerce-send" ${state.isLoading ? "disabled" : ""} aria-label="Send message">
1069
+ <button class="aicommerce-mic ${state.isRecording ? "aicommerce-recording" : ""}" ${state.isLoading ? "disabled" : ""} aria-label="${state.isRecording ? "Stop recording" : "Voice input"}">
1070
+ ${state.isRecording ? `
1071
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
1072
+ <rect x="6" y="6" width="12" height="12" rx="2"/>
1073
+ </svg>
1074
+ ` : `
1075
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1076
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
1077
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
1078
+ <line x1="12" y1="19" x2="12" y2="23"/>
1079
+ <line x1="8" y1="23" x2="16" y2="23"/>
1080
+ </svg>
1081
+ `}
1082
+ </button>
1083
+ <button class="aicommerce-send" ${state.isLoading || state.isRecording ? "disabled" : ""} aria-label="Send message">
862
1084
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
863
1085
  <path d="M22 2L11 13M22 2L15 22L11 13M22 2L2 9L11 13"/>
864
1086
  </svg>
@@ -873,6 +1095,27 @@ function createWidget(config) {
873
1095
  messagesEl.scrollTop = messagesEl.scrollHeight;
874
1096
  }
875
1097
  }
1098
+ function renderAudioPlayer(msg, index, isUser) {
1099
+ return `
1100
+ <div class="aicommerce-audio-player" data-message-index="${index}">
1101
+ <button class="aicommerce-audio-btn">
1102
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
1103
+ </button>
1104
+ <div class="aicommerce-audio-waveform">
1105
+ <div class="aicommerce-waveform-bars">
1106
+ ${(msg.waveformBars || Array(40).fill(10)).map((height) => `
1107
+ <div class="aicommerce-waveform-bar" style="height: ${height}%; background-color: ${isUser ? "rgba(255,255,255,0.4)" : "rgba(99,102,241,0.3)"}"></div>
1108
+ `).join("")}
1109
+ </div>
1110
+ <div class="aicommerce-audio-time">
1111
+ <span class="aicommerce-current-time">0:00</span>
1112
+ <span>${formatTime(msg.audioDuration || 0)}</span>
1113
+ </div>
1114
+ </div>
1115
+ <audio src="${msg.audioUrl}" preload="metadata"></audio>
1116
+ </div>
1117
+ `;
1118
+ }
876
1119
  function attachEventListeners() {
877
1120
  if (!container) return;
878
1121
  const launcherEl = container.querySelector(".aicommerce-launcher");
@@ -901,6 +1144,10 @@ function createWidget(config) {
901
1144
  }
902
1145
  });
903
1146
  }
1147
+ const micEl = container.querySelector(".aicommerce-mic");
1148
+ if (micEl) {
1149
+ micEl.addEventListener("click", () => handleMicClick());
1150
+ }
904
1151
  const productCards = container.querySelectorAll(".aicommerce-product-card");
905
1152
  productCards.forEach((card) => {
906
1153
  card.addEventListener("click", () => {
@@ -911,6 +1158,208 @@ function createWidget(config) {
911
1158
  }
912
1159
  });
913
1160
  });
1161
+ const sliders = container.querySelectorAll(".aicommerce-products");
1162
+ sliders.forEach((slider) => {
1163
+ let isDown = false;
1164
+ let startX = 0;
1165
+ let scrollLeft = 0;
1166
+ slider.addEventListener("mousedown", (e) => {
1167
+ isDown = true;
1168
+ slider.style.cursor = "grabbing";
1169
+ startX = e.pageX - slider.offsetLeft;
1170
+ scrollLeft = slider.scrollLeft;
1171
+ });
1172
+ slider.addEventListener("mouseleave", () => {
1173
+ isDown = false;
1174
+ slider.style.cursor = "grab";
1175
+ });
1176
+ slider.addEventListener("mouseup", () => {
1177
+ isDown = false;
1178
+ slider.style.cursor = "grab";
1179
+ });
1180
+ slider.addEventListener("mousemove", (e) => {
1181
+ if (!isDown) return;
1182
+ e.preventDefault();
1183
+ const x = e.pageX - slider.offsetLeft;
1184
+ const walk = (x - startX) * 2;
1185
+ slider.scrollLeft = scrollLeft - walk;
1186
+ });
1187
+ });
1188
+ const audioPlayers = container.querySelectorAll(".aicommerce-audio-player");
1189
+ audioPlayers.forEach((player) => {
1190
+ const audio = player.querySelector("audio");
1191
+ const btn = player.querySelector(".aicommerce-audio-btn");
1192
+ const bars = player.querySelectorAll(".aicommerce-waveform-bar");
1193
+ const timeDisplay = player.querySelector(".aicommerce-current-time");
1194
+ if (!audio || !btn) return;
1195
+ btn.addEventListener("click", () => {
1196
+ const isPlaying = !audio.paused;
1197
+ if (!isPlaying) {
1198
+ container?.querySelectorAll("audio").forEach((a) => {
1199
+ if (a !== audio && !a.paused) {
1200
+ a.pause();
1201
+ const parent = a.closest(".aicommerce-audio-player");
1202
+ const otherBtn = parent?.querySelector(".aicommerce-audio-btn");
1203
+ if (otherBtn) otherBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`;
1204
+ }
1205
+ });
1206
+ audio.play();
1207
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>`;
1208
+ } else {
1209
+ audio.pause();
1210
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`;
1211
+ }
1212
+ });
1213
+ audio.addEventListener("timeupdate", () => {
1214
+ if (timeDisplay) timeDisplay.textContent = formatTime(audio.currentTime);
1215
+ if (audio.duration) {
1216
+ const progress = audio.currentTime / audio.duration * 100;
1217
+ bars.forEach((bar, i) => {
1218
+ const barPos = i / bars.length * 100;
1219
+ if (barPos <= progress) {
1220
+ bar.style.backgroundColor = player.closest(".aicommerce-user") ? "rgba(255,255,255,1)" : "var(--aic-primary)";
1221
+ } else {
1222
+ bar.style.backgroundColor = player.closest(".aicommerce-user") ? "rgba(255,255,255,0.4)" : "rgba(99,102,241,0.3)";
1223
+ }
1224
+ });
1225
+ }
1226
+ });
1227
+ audio.addEventListener("ended", () => {
1228
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`;
1229
+ });
1230
+ const waveform = player.querySelector(".aicommerce-waveform-bars");
1231
+ if (waveform) {
1232
+ waveform.addEventListener("click", (e) => {
1233
+ const rect = waveform.getBoundingClientRect();
1234
+ const x = e.clientX - rect.left;
1235
+ const percent = x / rect.width;
1236
+ if (audio.duration) {
1237
+ audio.currentTime = percent * audio.duration;
1238
+ }
1239
+ });
1240
+ }
1241
+ });
1242
+ }
1243
+ async function handleMicClick() {
1244
+ if (state.isRecording) {
1245
+ if (mediaRecorder && mediaRecorder.state !== "inactive") {
1246
+ mediaRecorder.stop();
1247
+ }
1248
+ } else {
1249
+ try {
1250
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1251
+ audioChunks = [];
1252
+ mediaRecorder = new MediaRecorder(stream, {
1253
+ mimeType: MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/mp4"
1254
+ });
1255
+ mediaRecorder.ondataavailable = (e) => {
1256
+ if (e.data.size > 0) {
1257
+ audioChunks.push(e.data);
1258
+ }
1259
+ };
1260
+ mediaRecorder.onstop = async () => {
1261
+ stream.getTracks().forEach((track) => track.stop());
1262
+ if (audioChunks.length > 0) {
1263
+ const audioBlob = new Blob(audioChunks, { type: mediaRecorder?.mimeType || "audio/webm" });
1264
+ await handleAudioSend(audioBlob);
1265
+ }
1266
+ state.isRecording = false;
1267
+ render();
1268
+ };
1269
+ mediaRecorder.start();
1270
+ state.isRecording = true;
1271
+ render();
1272
+ } catch (error) {
1273
+ console.error("Failed to start recording:", error);
1274
+ state.messages.push({
1275
+ role: "assistant",
1276
+ content: "Unable to access microphone. Please check your permissions."
1277
+ });
1278
+ render();
1279
+ }
1280
+ }
1281
+ }
1282
+ async function handleAudioSend(audioBlob) {
1283
+ const audioUrl = URL.createObjectURL(audioBlob);
1284
+ let waveformBars = Array(40).fill(10);
1285
+ let audioDuration = 0;
1286
+ try {
1287
+ waveformBars = await analyzeAudio(audioBlob);
1288
+ const audio = new Audio(audioUrl);
1289
+ await new Promise((resolve) => {
1290
+ audio.onloadedmetadata = () => {
1291
+ audioDuration = audio.duration;
1292
+ resolve();
1293
+ };
1294
+ audio.onerror = () => resolve();
1295
+ });
1296
+ } catch (e) {
1297
+ console.error("Audio analysis failed", e);
1298
+ }
1299
+ state.messages.push({
1300
+ role: "user",
1301
+ content: "Voice message",
1302
+ audioUrl,
1303
+ audioDuration,
1304
+ waveformBars
1305
+ });
1306
+ state.isLoading = true;
1307
+ render();
1308
+ try {
1309
+ const response = await client.chatWithAudio(audioBlob);
1310
+ state.messages.push({
1311
+ role: "assistant",
1312
+ content: response.reply,
1313
+ products: response.products
1314
+ });
1315
+ if (resolvedConfig.onMessage) {
1316
+ resolvedConfig.onMessage("Voice message", response);
1317
+ }
1318
+ return response;
1319
+ } catch (error) {
1320
+ state.messages.push({
1321
+ role: "assistant",
1322
+ content: "Sorry, I encountered an error processing your voice message. Please try again."
1323
+ });
1324
+ throw error;
1325
+ } finally {
1326
+ state.isLoading = false;
1327
+ render();
1328
+ }
1329
+ }
1330
+ function isArabic(text) {
1331
+ return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
1332
+ }
1333
+ function formatTime(seconds) {
1334
+ const mins = Math.floor(seconds / 60);
1335
+ const secs = Math.floor(seconds % 60);
1336
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
1337
+ }
1338
+ async function analyzeAudio(blob) {
1339
+ try {
1340
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
1341
+ const arrayBuffer = await blob.arrayBuffer();
1342
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
1343
+ const channelData = audioBuffer.getChannelData(0);
1344
+ const bars = 40;
1345
+ const step = Math.floor(channelData.length / bars);
1346
+ const calculatedBars = [];
1347
+ for (let i = 0; i < bars; i++) {
1348
+ const start = i * step;
1349
+ const end = start + step;
1350
+ let sum = 0;
1351
+ for (let j = start; j < end; j++) {
1352
+ if (channelData[j]) sum += channelData[j] * channelData[j];
1353
+ }
1354
+ const rms = Math.sqrt(sum / step);
1355
+ const height = Math.min(100, Math.max(10, rms * 400));
1356
+ calculatedBars.push(height);
1357
+ }
1358
+ return calculatedBars;
1359
+ } catch (e) {
1360
+ console.error("Analysis error", e);
1361
+ return Array.from({ length: 40 }, () => 20 + Math.random() * 60);
1362
+ }
914
1363
  }
915
1364
  async function handleSend(message) {
916
1365
  state.messages.push({ role: "user", content: message });