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