@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/ai-commerce.min.js +217 -30
- package/dist/index.cjs +463 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.min.js.map +1 -1
- package/dist/index.mjs +463 -14
- package/dist/index.mjs.map +1 -1
- package/dist/widget.min.js +223 -36
- package/dist/widget.min.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
599
|
+
gap: 16px;
|
|
554
600
|
margin-top: 12px;
|
|
555
601
|
overflow-x: auto;
|
|
556
|
-
padding-bottom:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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-
|
|
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 });
|