@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.cjs
CHANGED
|
@@ -107,7 +107,10 @@ var init_client = __esm({
|
|
|
107
107
|
*/
|
|
108
108
|
detectBaseUrl() {
|
|
109
109
|
if (typeof window !== "undefined") {
|
|
110
|
-
|
|
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:
|
|
601
|
+
gap: 16px;
|
|
556
602
|
margin-top: 12px;
|
|
557
603
|
overflow-x: auto;
|
|
558
|
-
padding-bottom:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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-
|
|
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 });
|