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