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